package ru.yandex.wmtools.common.util;

import java.net.IDN;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import org.apache.commons.lang3.StringEscapeUtils;

import ru.yandex.common.util.xml.XmlConvertable;
import ru.yandex.wmtools.common.SupportedProtocols;
import ru.yandex.wmtools.common.data.info.WMUserInfo;
import ru.yandex.wmtools.common.data.wrappers.DateWrapper;
import ru.yandex.wmtools.common.data.wrappers.UserInfoWrapper;

/**
 * Abstract XML wrapper of arbitrary data.
 * Provides useful methods for converting data to XML format.
 *
 * @author senin
 * @author baton
 * @author ailyin
 * @author yakushev
 */
public abstract class XmlDataWrapper<T> implements XmlConvertable {
    private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");
    private static final SimpleDateFormat DATE_TIME_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");

    protected final T data;
    private final String name;
    private List<String> attributeNames;
    private List<String> attributeValues;

    public XmlDataWrapper(T data, String name, String ... attributes) {
        this.data = data;
        this.name = name;

        if (attributes != null) {
            if ((attributes.length % 2) != 0) {
                throw new IllegalArgumentException(
                        "Number of attribute params must be even, because these are pairs <name>, <value>");
            }

            initAttributeLists();
            for (int i = 0; i < attributes.length - 1; i += 2) {
                if (attributes[i + 1] != null) {
                    addAttribute(attributes[i], attributes[i + 1]);
                }
            }
        }
    }

    public XmlDataWrapper(T data, String name) {
        this(data, name, (String[]) null);
    }

    public XmlDataWrapper(T data) {
        this(data, null);
    }

    private void initAttributeLists() {
        if (attributeNames == null) {
            attributeNames = new ArrayList<String>();
        }
        if (attributeValues == null) {
            attributeValues = new ArrayList<String>();
        }
    }

    protected final void addAttribute(String name, String value) {
        addAttribute(name, value, false);
    }

    protected final void addAttribute(final String name, final String value, final boolean needEscape) {
        if (name == null) {
            throw new IllegalArgumentException("name is null");
        }
        if (value == null) {
            throw new IllegalArgumentException("value is null");
        }

        initAttributeLists();
        attributeNames.add(name);
        attributeValues.add(needEscape ? StringEscapeUtils.escapeXml10(value) : value);
    }

    private void putAttribute(StringBuilder result, String attrName, String attrValue) {
        result.append(" ").append(attrName).append("=\"").append(attrValue).append("\"");
    }

    @Override
    public final void toXml(StringBuilder result) {
        putStartTag(result);
        doToXml(result);
        putEndTag(result);
    }

    protected abstract void doToXml(StringBuilder result);

    private void putStartTag(StringBuilder result) {
        if (name == null) {
            return;
        }

        result.append("<").append(StringEscapeUtils.escapeXml10(name));
        if (attributeNames != null) {
            for (int i = 0; i < attributeNames.size(); i++) {
                putAttribute(result, attributeNames.get(i), attributeValues.get(i));
            }
        }
        result.append(">");
    }

    private void putEndTag(StringBuilder result) {
        if (name == null) {
            return;
        }

        result.append("</").append(StringEscapeUtils.escapeXml10(name)).append(">");
    }

    protected void putTag(StringBuilder result, String name, String value, String ... attributes) {
        if (value != null) {
            putOpenTag(result, name, attributes);
            result.append(StringEscapeUtils.escapeXml10(value));
            putCloseTag(result, name);
        } else {
            putSimpleTag(result, name, attributes);
        }
    }

    protected void putComplexTag(StringBuilder result, String name, XmlDataWrapper innerTag, String ... attributes) {
        if (innerTag != null) {
            putOpenTag(result, name, attributes);
            innerTag.doToXml(result);
            putCloseTag(result, name);
        } else {
            putSimpleTag(result, name, attributes);
        }
    }

    protected void putSimpleTag(StringBuilder result, String name, String ... attributes) {
        startTag(result, name, attributes);
        result.append("/>");
    }

    protected static void putOpenTag(StringBuilder result, String name, String ... attributes) {
        startTag(result, name, attributes);
        result.append(">");
    }

    private static void startTag(StringBuilder result, String name, String... attributes) {
        if ((attributes.length % 2) != 0) {
            throw new IllegalArgumentException(
                    "Number of attribute params must be even, because these are pairs <name>, <value>");
        }
        result.append("<").append(StringEscapeUtils.escapeXml10(name));
        for (int i = 0; i < attributes.length - 1; i += 2) {
            String attrName = attributes[i];
            String attrValue = attributes[i + 1];
            if (attrName == null) {
                throw new IllegalArgumentException("attribute name is null");
            }
            if (attrValue == null) {
                throw new IllegalArgumentException("attribute value is null");
            }
            result.append(" ").
                   append(StringEscapeUtils.escapeXml10(attrName)).
                   append("=\"").
                   append(StringEscapeUtils.escapeXml10(attrValue)).
                   append("\"");
        }
    }

    protected static void putCloseTag(StringBuilder result, String name) {
        result.append("</").append(StringEscapeUtils.escapeXml10(name)).append(">");
    }

    protected void putIntegerTag(StringBuilder result, String name, Integer value) {
        if (value != null) {
            putTag(result, name, Integer.toString(value));
        }
    }

    protected void putDoubleTag(StringBuilder result, String name, Double value) {
        if (value != null) {
            putTag(result, name, Double.toString(value));
        }
    }

    protected void putLongTag(StringBuilder result, String name, Long value) {
        if (value != null) {
            putTag(result, name, Long.toString(value));
        }
    }

    protected void putBooleanTag(StringBuilder result, String name, Boolean value) {
        if (value != null) {
            putTag(result, name, Boolean.toString(value));
        }
    }

    protected void putExtendedDateTimeTag(StringBuilder result, String name, Date value) {
        if (value != null) {
            new DateWrapper(name, value).toXml(result);
        }
    }

    protected void putExtendedUserTag(StringBuilder result, String name, WMUserInfo data) {
        if (data != null) {
            new UserInfoWrapper(data, name).toXml(result);
        }
    }

    protected void putDateTag(StringBuilder result, String name, Date value) {
        if (value != null) {
            putTag(result, name, getDateFormat().format(value));
        }
    }

    protected void putDateTimeTag(StringBuilder result, String name, Date value) {
        if (value != null) {
            putTag(result, name, getDateTimeFormat().format(value));
        }
    }

    protected void putUnixTimestampTag(StringBuilder result, String name, Date value) {
        if (value != null) {
            putTag(result, name, getUnixTimestamp(value));
        }
    }

    protected void putHostnameTag(StringBuilder result, String name, String value) {
        if (value != null) {
            String decodedUrl = value;
            try {
                URL url = SupportedProtocols.getURL(value);
                String decodedHost = IDN.toUnicode(url.getHost());
                if (!decodedHost.equals(url.getHost())) {
                    decodedUrl = value.replace(url.getHost(), decodedHost);
                }
            } catch (MalformedURLException e) {
                // ignore (input  value will be used)
            } catch (URISyntaxException e) {
                // ignore (input  value will be used)
            } catch (SupportedProtocols.UnsupportedProtocolException e) {
                // ignore (input  value will be used)
            }

            putTag(result, name, decodedUrl);
        }
    }

    public static SimpleDateFormat getDateFormat() {
        return DATE_FORMAT;
    }

    public static SimpleDateFormat getDateTimeFormat() {
        return DATE_TIME_FORMAT;
    }

    public static String getUnixTimestamp(Date date) {
        if (date == null) {
            return null;
        }
        return Long.toString(date.getTime() / 1000);
    }
}
