package net.northking.cctp.element.core;

import net.northking.cctp.element.core.annotation.Autowired;
import net.northking.cctp.element.core.exception.AssertException;
import net.northking.cctp.element.core.exception.ComponentRuntimeException;
import net.northking.cctp.element.core.exception.KeywordNotFoundException;
import net.northking.cctp.element.util.AtpUtils;
import net.northking.cctp.element.util.Javadoc2Libdoc;
import net.northking.cctp.element.core.keyword.Keyword;
import net.northking.cctp.element.core.keyword.KeywordArgument;
import net.northking.cctp.element.core.keyword.KeywordInfo;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.util.*;

/**
 * 组件库基类
 *
 * @author fengxiang.zhang
 * @since 1.0
 */
public abstract class AbstractLibrary implements Library, InitializingBean {

    /**
     * 历史版本变更说明markdown文件名称
     */
    private static final String CHANGE_LOG_MARKDOWN_LOCATION = "changelog.md";

    private final static Logger logger = LoggerFactory.getLogger(AbstractLibrary.class);

    @Autowired
    protected LibraryContext context;

    public abstract Javadoc2Libdoc getLibDoc();

    @Override
    public String getIntro() {
        Javadoc2Libdoc libDoc = getLibDoc();
        if (libDoc != null) {
            return libDoc.getKeywordDocumentation(Javadoc2Libdoc.LIBRARY_INTRO_KEY);
        }
        return "";
    }

    @Override
    public String getChangeLog() {
        InputStream inputStream = context.getClassLoader().getResourceAsStream(CHANGE_LOG_MARKDOWN_LOCATION);
        String changeLog = "";
        if (inputStream == null) {
            logger.warn("读取库版本变更历史文件{}失败.", CHANGE_LOG_MARKDOWN_LOCATION);
            return changeLog;
        }
        try {
            changeLog = AtpUtils.readWithLineFeed(inputStream, "utf-8");
        } catch (IOException e) {
            logger.warn("读取库版本变更历史文件{}异常：{}", CHANGE_LOG_MARKDOWN_LOCATION, e);
        }
        return changeLog;
    }

    @Override
    public Object runKeyword(String keywordName, Map<String, Object> dataBus)
            throws Exception {
        try {
            logger.debug("运行组件-> 库:[{}],类别:[{}], 版本:[{}], 组件:[{}], 参数:{}", getName(), getType(), getVersion(), keywordName,
                    getKeywordParams(keywordName, dataBus));
            return getKeyword(keywordName).execute(dataBus, context.getBean(IExecuteContext.class));
        } catch (Exception e) {
            if (e instanceof InvocationTargetException) {
                Throwable cause = ((InvocationTargetException) e).getTargetException();
                if (cause instanceof AssertException) {
                    throw new AssertException(cause.getMessage(), cause);
                }
                throw new ComponentRuntimeException(cause.getMessage(), cause);
            }
            throw e;
        }
    }

    @Override
    public Object runKeyword(String keywordName, IExecuteContext context)
            throws Exception {
        try {
            Map<String, Object> dataBus = getDataBusFromContext(keywordName, context);
            logger.debug("运行组件-> 库:[{}],类别:[{}], 版本:[{}], 组件:[{}], 参数:{}", getName(), getType(), getVersion(), keywordName,
                    getKeywordParams(keywordName, dataBus));
            return getKeyword(keywordName).execute(dataBus, context);
        } catch (Exception e) {
            if (e instanceof InvocationTargetException) {
                Throwable cause = ((InvocationTargetException) e).getTargetException();
                if (cause instanceof AssertException) {
                    throw new AssertException(cause.getMessage(), cause);
                }
                throw new ComponentRuntimeException(cause.getMessage(), cause);
            }
            throw e;
        }
    }

    @Override
    public String[] getKeywordNames() {
        List<String> keywordNames = new ArrayList<>();
        keywordNames.addAll(context.getKeywords().keySet());
        return keywordNames.toArray(new String[keywordNames.size()]);
    }

    @Override
    public String getKeywordDocumentation(String keywordName) {
        try {
            String keywordDocumentation = null;
            Javadoc2Libdoc libDoc = getLibDoc();
            if (libDoc != null) {
                keywordDocumentation = libDoc.getKeywordDocumentation(keywordName);
            }
            if (keywordDocumentation == null) {
                keywordDocumentation = getKeyword(keywordName).getDocumentation();
            }
            return keywordDocumentation;
        } catch (Exception e) {
            logger.error("Get keyword [{}] documentation exception.", keywordName);
            return "";
        }
    }

    @Override
    public List<KeywordArgument> getKeywordArguments(String keywordName)
            throws Exception {
        return getKeyword(keywordName).getArguments();
    }

    @Override
    public Map<String, KeywordArgument> getKeywordArgumentMap(String keywordName)
            throws Exception {
        return getKeyword(keywordName).getArgumentMap();
    }

    @Override
    public String getKeywordAlias(String keywordName) throws KeywordNotFoundException {
        return getKeyword(keywordName).getAlias();
    }

    @Override
    public List<KeywordInfo> getAllKeywords() {
        List<KeywordInfo> keywordInfoList = new ArrayList<KeywordInfo>();
        Map<String, Keyword> keywords = context.getKeywords();
        for (Map.Entry<String, Keyword> entry : keywords.entrySet()) {
            keywordInfoList.add(getKeywordInfo(entry.getValue()));
        }
        return keywordInfoList;
    }

    @Override
    public boolean existKeyword(String keywordName) {
        return context.getKeywords().containsKey(keywordName);
    }

    @Override
    public Iterator<KeywordInfo> iterator() {
        Iterator keywordInfoIterator = new Iterator() {
            private final Iterator<Map.Entry<String, Keyword>> iterator = context.getKeywords().entrySet().iterator();

            @Override
            public boolean hasNext() {
                return iterator.hasNext();
            }

            @Override
            public Object next() {
                Map.Entry<String, Keyword> entry = iterator.next();
                return getKeywordInfo(entry.getValue());
            }
        };
        return keywordInfoIterator;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        // Please override if necessary
    }

    // ###################################################################
    // Internal
    // ###################################################################

    private KeywordInfo getKeywordInfo(Keyword keyword) {
        KeywordInfo info = new KeywordInfo();
        String name = keyword.getName();
        info.setKeywordName(name);
        info.setKeywordAlias(keyword.getAlias());
        info.setKeywordDocumentation(getKeywordDocumentation(name));
        info.setKeywordArguments(keyword.getArguments());
        info.setKeywordReturn(keyword.getReturn());
        info.setDeprecated(keyword.isDeprecated());
        info.setHidden(keyword.isHidden());
        info.setCategory(keyword.getCategory());
        info.setAttributes(keyword.getAttributes());
        info.setCommonlyUse(keyword.isCommonlyUse());
        return info;
    }

    private Keyword getKeyword(String keywordName) throws KeywordNotFoundException {
        Keyword keyword = context.getKeywords().get(keywordName);
        if (keyword == null) {
            throw new KeywordNotFoundException(String.format("keyword %s is not found.", keywordName));
        }
        return keyword;
    }

    private Map<String, Object> getDataBusFromContext(String keywordName, IExecuteContext context) throws KeywordNotFoundException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {
        Map<String, Object> dataBus = new HashMap<>();
        Keyword keyword = getKeyword(keywordName);
        List<KeywordArgument> arguments = keyword.getArguments();
        String name;
        for (KeywordArgument argument : arguments) {
            name = argument.getName();
            switch (argument.getScope()) {
                case ARGS: {
                    IScriptStep step = context.getCurrentStep();
                    dataBus.put(name, step.getArgs().get(name));
                    break;
                }
                case STEP: {
                    IScriptStep step = context.getCurrentStep();
                    Object value = step.getClass().getMethod("get" + StringUtils.capitalize(name)).invoke(step);
                    dataBus.put(name, value);
                    break;
                }
                case VARIABLES: {
                    dataBus.put(name, context.getVariable(name));
                    break;
                }
                case CONTEXT: {
                    dataBus.put(name, context.getContextVariable(name));
                    break;
                }
                case OUTPUT: {
                    IScriptStep step = context.getCurrentStep();
                    Map<String, String> map = step.getOutput();
                    dataBus.put(name, map.get(name));
                    break;
                }
            }
        }
        return dataBus;
    }

    private String getKeywordParams(String keywordName, Map<String, Object> dataBus) throws Exception {
        StringBuffer sb = new StringBuffer();
        List<KeywordArgument> arguments = getKeywordArguments(keywordName);
        sb.append("{");
        int i = 1, size = arguments.size();
        for (KeywordArgument argument : arguments) {
            sb.append(argument.getComment() + "(" + argument.getName() + ")=");
            sb.append(argument.isHideSensitive() ? "******" : object2String(dataBus.get(argument.getName())));
            if (i < size) {
                sb.append(", ");
            }
            i++;
        }
        sb.append("}");
        return sb.toString();
    }

    private static String object2String(Object object) {
        if (object == null) {
            return "null";
        }
        return StringUtils.abbreviate(object.toString(), 500);
    }
}