package net.northking.cctp.element.xml.keywords;

import cn.hutool.core.util.ReUtil;
import com.alibaba.fastjson.JSON;
import net.northking.cctp.element.core.type.InputType;
import net.northking.cctp.element.xml.enums.TypeConversionGetEnum;
import net.northking.cctp.element.xml.enums.TypeConversionRemoveEnum;
import net.northking.cctp.element.xml.enums.TypeConversionSetEnum;
import net.northking.cctp.element.xml.exception.XMLException;
import net.northking.cctp.element.core.IExecuteContext;
import net.northking.cctp.element.core.annotation.*;
import net.northking.cctp.element.core.type.DataType;
import net.northking.cctp.element.core.type.ParamScope;
import org.dom4j.*;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.XMLWriter;

import java.io.IOException;
import java.io.StringWriter;
import java.util.*;

/**
 * XML组件
 *
 * @author fengxiang.zhang
 * @since 1.0
 */
@Keywords
public class XML {

    /**
     * <p>在原有xml元素结构对象上添加新的元素节点。</p>
     * Example:
     * <pre>
     * xmlObject = &lt;root&gt;&lt;/root&gt;
     * addElement(xmlObject, '/root', '&lt;c1/&gt;') => &lt;root&gt;&lt;c1/&gt;&lt;/root&gt;
     * </pre>
     *
     * @param source  xml元素对象
     * @param xpath   xpath表达式
     * @param element xml字符串
     * @return 组件执行结果
     */
    @Keyword(alias = "添加元素")
    @Return(name = "result", comment = "组件执行结果", type = DataType.OBJECT)
    public Object addElement(
            @Argument(name = "source", comment = "xml对象", type = DataType.OBJECT) Object source,
            @Argument(name = "xpath", comment = "xpath表达式", type = DataType.STRING) String xpath,
            @Argument(name = "element", comment = "xml字符串", type = DataType.OBJECT) Object element,
            @Argument(name = "result", comment = "新xml字符串", type = DataType.OBJECT, scope = ParamScope.OUTPUT) String result,
            IExecuteContext context) {
        Node target = parseElement(source);
        List<Element> elements = selectElements(target, xpath, true);
        Element ele = parseElement(element);
        for (int i = 0; i < elements.size(); i++) {
            if (i == 0) {
                elements.get(i).add(ele);
            } else {
                elements.get(i).add(ele.createCopy());
            }
        }
        Object data = elementToString(target, false);
        save2ContentText(result, context, data);
        return data;
    }

    /**
     * <p>获取元素相关属性、文本、元素。</p>
     * Example:
     * <pre>
     * xml = &lt;root id="10"&gt;&lt;c1 id="10"&gt;10&lt;/c1&gt;&lt;c1 id="20"&gt;20&lt;/c1&gt;&lt;/root&gt;
     *
     * getElement("0",xml,"/root/c1")   => &lt;c1 id="10"&gt;10&lt;/c1&gt;
     * getElement("1",xml,"/root/c1")   => {"id":"10"}
     * getElement("2",xml,"/root/c1")   => 10
     * getElement("3",xml,"/root/c1")   => ["&lt;c1 id=\"10\"&gt;10&lt;/c1&gt;","&lt;c1 id=\"20\"&gt;20&lt;/c1&gt;"]
     * getElement("4",xml,"/root/c1")   => ["10","20"]
     * getElement("5",xml,"/root")      => ["&lt;c1 id=\"10\"&gt;10&lt;/c1&gt;","&lt;c1 id=\"20\"&gt;20&lt;/c1&gt;"]
     * </pre>
     *
     * @param type   类型("0"-获取xpath匹配的第一个元素;"1"-获取xpath匹配的第一个元素的属性;"2"-获取xpath匹配的第一个元素的文本;
     *               "3"-获取xpath匹配的所有元素;"4"-获取xpath匹配的所有元素的文本;"5"-获取xpath匹配的子元素;)
     * @param source xml对象
     * @param xpath  xpath表达式
     * @return 组件执行结果
     */
    @Keyword(alias = "获取元素")
    @Return(name = "result", comment = "组件执行结果", type = DataType.OBJECT)
    public Object getElement(
            @Argument(name = "type", comment = "类型", type = DataType.ENUM, enumValues = TypeConversionGetEnum.class, inputType = InputType.select) String type,
            @Argument(name = "source", comment = "xml对象", type = DataType.OBJECT) Object source,
            @Argument(name = "xpath", comment = "xpath表达式", type = DataType.STRING) String xpath,
            @Argument(name = "value", comment = "组件返回值", type = DataType.STRING, scope = ParamScope.OUTPUT, defaultValue = "") String value,
            IExecuteContext context) {
        TypeConversionGetEnum typeConversionEnum = TypeConversionGetEnum.from(type);
        Node target = parseElement(source);
        Object data = null;
        switch (typeConversionEnum) {
            case GetFirstElemnt: {
                Element element = selectElement(target, xpath, true);
                data = elementToString(element, false);
                break;
            }
            case GetAttrOfFE: {
                Element element = selectElement(target, xpath, true);
                Map<String, String> result = new HashMap<>();
                if (element != null) {
                    for (Iterator<Attribute> iterator = element.attributeIterator(); iterator.hasNext(); ) {
                        Attribute attribute = iterator.next();
                        result.put(attribute.getName(), attribute.getValue());
                    }
                }
                data = JSON.toJSONString(result);
                break;
            }
            case GetTextOfFE: {
                Element element = selectElement(target, xpath, true);
                if (element != null) {
                    data = element.getText();
                }
                break;
            }
            case GetAllElemnt: {
                List<Element> elements = selectElements(target, xpath, true);
                List<String> elementStrList = new ArrayList<>();
                elements.forEach(r -> elementStrList.add(elementToString(r, false)));
                data = JSON.toJSONString(elementStrList);
                break;
            }
            case GetTextOfAE: {
                List<Element> elements = selectElements(target, xpath, true);
                List<String> result = new ArrayList<>();
                for (Element element : elements) {
                    result.add(element.getText());
                }
                data = JSON.toJSONString(result);
                break;
            }
            case GetChildElemnt: {
                List<Element> elements = Objects.requireNonNull(selectElement(target, xpath, true)).elements();
                List<String> elementStrList = new ArrayList<>();
                elements.forEach(r -> elementStrList.add(elementToString(r, false)));
                data = JSON.toJSONString(elementStrList);
                break;
            }
            default:
                throw new IllegalArgumentException("未知的类型:" + type);
        }
        save2ContentText(value, context, data);
        return data;
    }

    /**
     * <p>设置xpath匹配元素的相关文本、属性。</p>
     * Example:
     * <pre>
     * xml = &lt;root id="10"&gt;&lt;c1 id="10"&gt;10&lt;/c1&gt;&lt;c1 id="20"&gt;20&lt;/c1&gt;&lt;/root&gt;
     *
     * setElement("0", xml, "/root/c1",null,"666")    =>  &lt;root id="10"&gt;&lt;c1 id="10"&gt;666&lt;/c1&gt;&lt;c1 id="20"&gt;20&lt;/c1&gt;&lt;/root&gt;
     * setElement("1", xml, "/root/c1",null,"666")    =>  &lt;root id="10"&gt;&lt;c1 id="10"&gt;666&lt;/c1&gt;&lt;c1 id="20"&gt;666&lt;/c1&gt;&lt;/root&gt;
     * setElement("2", xml, "/root/c1","field","666") =>  &lt;root id="10"&gt;&lt;c1 id="10" field="666">10&lt;/c1&gt;&lt;c1 id="20"&gt;20&lt;/c1&gt;&lt;/root&gt;
     * setElement("3", xml, "/root/c1","field","666") =>  &lt;root id="10"&gt;&lt;c1 id="10" field="666">10&lt;/c1&gt;&lt;c1 id="20" field="666"&gt;20&lt;/c1&gt;&lt;/root&gt;
     * </pre>
     *
     * @param type   类型("0"-设置xpath匹配的第一个元素的文本;"1"-设置xpath匹配的所有元素的文本;
     *               "2"-设置xpath匹配的第一个元素的属性;"3"-设置xpath匹配的所有元素的属性)
     * @param source xml对象
     * @param xpath  xpath表达式
     * @param name   属性名
     * @param value  text值
     * @return 组件执行结果
     */
    @Keyword(alias = "设置元素")
    @Return(name = "result", comment = "组件执行结果", type = DataType.OBJECT)
    public Object setElement(
            @Argument(name = "type", comment = "类型", type = DataType.ENUM, enumValues = TypeConversionSetEnum.class, inputType = InputType.select) String type,
            @Argument(name = "source", comment = "xml对象", type = DataType.OBJECT) Object source,
            @Argument(name = "xpath", comment = "xpath表达式", type = DataType.STRING) String xpath,
            @Argument(name = "name", comment = "属性名", type = DataType.STRING) String name,
            @Argument(name = "value", comment = "text值", type = DataType.STRING) String value,
            @Argument(name = "result", comment = "组件返回值", type = DataType.STRING, scope = ParamScope.OUTPUT, defaultValue = "") String result,
            IExecuteContext context) {
        TypeConversionSetEnum typeConversionEnum = TypeConversionSetEnum.from(type);
        Node target = parseElement(source);
        Object data;
        switch (typeConversionEnum) {
            case setFirstElementText: {
                Element element = selectElement(target, xpath, true);
                if (element != null) {
                    element.setText(value);
                }
                break;
            }
            case setAllElementText: {
                List<Element> elements = selectElements(target, xpath, true);
                for (Element element : elements) {
                    element.setText(value);
                }
                break;
            }
            case setFirstElementField: {
                Element element = selectElement(target, xpath, true);
                Attribute attribute = null;
                if (element != null) {
                    attribute = element.attribute(name);
                }
                if (attribute == null) {
                    if (element != null) {
                        element.addAttribute(name, value);
                    }
                } else {
                    attribute.setValue(value);
                }
                break;
            }
            case setAllElementField: {
                List<Element> elements = selectElements(target, xpath, true);
                Attribute attribute;
                for (Element element : elements) {
                    attribute = element.attribute(name);
                    if (attribute == null) {
                        element.addAttribute(name, value);
                    } else {
                        attribute.setValue(value);
                    }
                }
                break;
            }
            default:
                throw new IllegalArgumentException("未知的类型:" + type);
        }
        data = elementToString(target, false);
        save2ContentText(result, context, data);
        return data;
    }

    /**
     * <p>删除Xpath匹配的元素的相关元素、属性、文本</p>
     * Example:
     * <pre>
     * xml = &lt;root id="10"&gt;&lt;c1 id="10"&gt;10&lt;/c1&gt;&lt;c1 id="20"&gt;20&lt;/c1&gt;&lt;/root&gt;
     *
     * removeElement("0", xml ,"/root/c1",null)     =>  &lt;root id="10"&gt;&lt;c1 id="20">20&lt;/c1&gt;&lt;/root&gt;
     * removeElement("1", xml ,"/root/c1","id")     =>  &lt;root id="10"&gt;&lt;c1&gt;10&lt;/c1&gt;&lt;c1 id="20">20&lt;/c1&gt;&lt;/root&gt;
     * removeElement("2", xml ,"/root/c1",null)     =>  &lt;root id="10"&gt;&lt;/root&gt;
     * removeElement("3", xml ,"/root/c1","id")     =>  &lt;root id="10"&gt;&lt;c1>10&lt;/c1&gt;&lt;c1>20&lt;/c1&gt;&lt;/root&gt;
     * </pre>
     *
     * @param type   类型("0"-删除xpath匹配的第一个元素;"1"-删除xpath匹配的第一个元素的属性;
     *               "2"-删除xpath匹配的所有元素;"3"-删除xpath匹配的所有元素的属性)
     * @param source xml对象
     * @param xpath  xpath表达式
     * @param name   属性名
     * @return 组件执行结果
     */
    @Keyword(alias = "删除元素")
    @Return(name = "result", comment = "组件执行结果", type = DataType.OBJECT)
    public Object removeElement(
            @Argument(name = "type", comment = "类型", type = DataType.ENUM, enumValues = TypeConversionRemoveEnum.class, inputType = InputType.select) String type,
            @Argument(name = "source", comment = "xml对象", type = DataType.OBJECT) Object source,
            @Argument(name = "xpath", comment = "xpath表达式", type = DataType.STRING) String xpath,
            @Argument(name = "name", comment = "属性名", type = DataType.STRING) String name,
            @Argument(name = "value", comment = "组件返回值", type = DataType.STRING, scope = ParamScope.OUTPUT, defaultValue = "") String value,
            IExecuteContext context) {
        TypeConversionRemoveEnum typeConversionEnum = TypeConversionRemoveEnum.from(type);
        Node target = parseElement(source);
        Object data;
        switch (typeConversionEnum) {
            case removeFirstElement: {
                Element element = selectElement(target, xpath, true);
                if (element != null) {
                    element.detach();
                }
                break;
            }
            case removeFieldOfFE: {
                Element element = selectElement(target, xpath, true);
                if (element != null) {
                    element.attribute(name).detach();
                }
                break;
            }
            case removeAllElement: {
                List<Element> elements = selectElements(target, xpath, true);
                for (Element element : elements) {
                    element.detach();
                }
                break;
            }
            case removeFieldOfAE: {
                List<Element> elements = selectElements(target, xpath, true);
                for (Element element : elements) {
                    element.attribute(name).detach();
                }
                break;
            }
            default:
                throw new IllegalArgumentException("未知的类型:" + type);
        }
        data = elementToString(target, false);
        save2ContentText(value, context, data);
        return data;
    }

    /**
     * <p>将xml格式字符串解析成xml元素结构对象。</p>
     * Example:
     * <pre>
     * xmlObject = parseXml('&lt;root&gt;&lt;/root&gt;')
     * </pre>
     *
     * @param xml xml字符串
     * @return xml元素结构对象
     */
    //@Keyword(alias = "解析为XML对象")
    @Return(name = "document", comment = "xml对象", type = DataType.OBJECT)
    public Document parseXml(
            @Argument(name = "xml", comment = "xml字符串", type = DataType.STRING) String xml,
            @Argument(name = "value", comment = "组件返回值", type = DataType.STRING, scope = ParamScope.OUTPUT, defaultValue = "") String value,
            IExecuteContext executeContext) {
        save2ContentText(value, executeContext, parseText(xml));
        return parseText(xml);
    }

    /**
     * <p>XML对象内容转换为字符串。</p>
     * Example:
     * <pre>
     * xmlObject = parseXml('&lt;root&gt;&lt;c1 id="1" index="1"/&gt;&lt;c1 id="2"/&gt;&lt;/root&gt;')
     * xml = elementToString(xmlObject, false)
     * =>
     * xml = '&lt;root&gt;&lt;c1 id="1" index="1"/&gt;&lt;c1 id="2"/&gt;&lt;/root&gt;'
     *
     * xml = elementToString(xmlObject, true)
     * =>
     * xml = '&lt;root&gt;
     *          &lt;c1 id="1" index="1"/&gt;
     *          &lt;c1 id="2"/&gt;
     *        &lt;/root&gt;'
     * </pre>
     *
     * @param source xml文对象
     * @param pretty 格式美化
     * @return xml字符串
     */
    //@Keyword(alias = "XML对象转换为字符串")
    @Return(name = "xml", comment = "xml字符串", type = DataType.STRING)
    public String elementToString(
            @Argument(name = "source", comment = "xml文对象", type = DataType.OBJECT) Node source,
            @Argument(name = "pretty", comment = "格式美化", type = DataType.BOOLEAN, required = false,
                    defaultValue = "false") boolean pretty) {
        if (source == null) {
            return null;
        }
        if (!pretty) {
            return source.asXML();
        }
        StringWriter writer = new StringWriter();
        XMLWriter output = new XMLWriter(writer, OutputFormat.createPrettyPrint());
        try {
            output.write(source);
        } catch (IOException e) {
            throw new XMLException(e);
        } finally {
            try {
                writer.close();
            } catch (IOException e) {
            }
            try {
                output.close();
            } catch (IOException e) {
            }
        }
        return writer.toString();
    }

    // ##########################################################################################
    // Internal Methods
    // ##########################################################################################

    private Document parseText(String xml) {
        try {
            return DocumentHelper.parseText(xml);
        } catch (DocumentException e) {
            throw new XMLException("非法的XML格式字符串", e);
        }
    }

    private Element selectElement(Node parent, String xpath, boolean require) {
        List<Node> nodes = selectNodes(parent, xpath, Node.ELEMENT_NODE, false, require);
        if (nodes.size() == 0) {
            return null;
        }
        return (Element) nodes.get(0);
    }

    private List<Element> selectElements(Node parent, String xpath, boolean require) {
        List<Node> nodes = selectNodes(parent, xpath, Node.ELEMENT_NODE, false, require);
        List<Element> result = new ArrayList<>();
        for (Node node : nodes) {
            result.add((Element) node);
        }
        return result;
    }

    private List<Node> selectNodes(Node parent, String xpath, int nodeType, boolean firstOnly, boolean require) {
        List<Node> result = new ArrayList<>();
        List<Node> nodes = parent.selectNodes(xpath);
        for (Node node : nodes) {
            if (node.getNodeType() == nodeType) {
                result.add(node);
                if (firstOnly) {
                    break;
                }
            }
        }
        if (result.size() == 0 && require) {
            throw new XMLException(String.format("未找到匹配的节点元素:%s", xpath));
        }
        return result;
    }

    private Element parseElement(Object object) {
        if (object instanceof Element) {
            return ((Element) object).createCopy();
        } else {
            return parseText(object.toString()).getRootElement();
        }
    }

    public static void save2ContentText(String value, IExecuteContext executeContext, Object s) {
        //将值塞入到组件返回值中
        if (value.contains("#{") && value.contains("}")) {
            String keyValue = ReUtil.get("(?<=\\#\\{)(.+?)(?=\\})", value, 1);
            // #{} 全局变量
            executeContext.setProjectGlobalVariable(keyValue, s.toString());
        }
        if (value.contains("${") && value.contains("}")) {
            String keyValue = ReUtil.get("(?<=\\$\\{)(.+?)(?=\\})", value, 1);
            // ${} 全局变量
            executeContext.setVariable(keyValue, s.toString());
        }
    }

}