package com.mozartframework.util;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.SAXParseException;

import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.*;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

import javax.xml.transform.TransformerException;

import com.mozartframework.db.base.MAttribute;

public class ErrorCodeException extends ADVException implements ADVExceptionCode, XMLObject {

    private static final long serialVersionUID = 0xef8c047be685729dL;

    /** Default element name for {@link #getDOMElement(org.w3c.dom.Document)} "error" **/
    public transient static final String ERROR_ELEMENT_NAME = "error";
    public transient static final String ERRORS_ELEMENT_NAME = "errors";
	public transient static final ErrorCodeException OK = new ErrorCodeException(Integer.MIN_VALUE, "OK");
	public transient static final ErrorCodeException STARTED = new ErrorCodeException(Integer.MIN_VALUE + 1, "STARTED");

    private int _code = UNKNOWN_ERROR;

    private Map _attrs = new TreeMap();
    private transient static Map<Integer,String> ERR_CODES;
    public static final String DATABASE = "database";
    public static final String ATTR = "attr";
    public static final String OBJECT = "object";
    public static final String TRIGGER = "trigger";
    public static final String LINK = "link";
    public static final String SYSTEM_ID = "system-id";
    public static final String SEARCH_ATTRS = "search-attrs";

    static {
        ERR_CODES = createCodeToNameMap();
    }

    /**
     *
     * @param code constant from {@link com.mozartframework.util.ADVExceptionCode}
     * @param msg
     */
    public ErrorCodeException(int code, String msg) {
        super(msg);
        setCode(code);
    }

    public ErrorCodeException(String msg) {
        super(msg);
    }

    /**
     *
     * @param code constant from {@link com.mozartframework.util.ADVExceptionCode}
     * @param e parent exception
     */
    public ErrorCodeException(int code, Throwable e) {
        this(e);
        setCode(code);
    }

    public ErrorCodeException(Throwable e) {
        super(e);
        setCode(e);
        setAttrs(e);
    }

    /**
     *
     * @param code constant from {@link com.mozartframework.util.ADVExceptionCode}
     * @param msg
     * @param e parent exception
     */
    public ErrorCodeException(int code, String msg, Throwable e) {
        super(msg, e);
        setCode(code);
        setAttrs(e);
    }

    /**
     * Конструктор
     * @param msg если instanceof ErrorCodeException, то наследует атрибуты
     * @param e если instanceof ErrorCodeException, то наследует атрибуты
     */
    public ErrorCodeException(String msg, Throwable e) {
        super(msg, e);
        setCode(e);
        setAttrs(e);
    }

    private void setAttrs(Throwable t) {
    	updateXmlLocationAttrs(t);
        if (t instanceof ErrorCodeException) {
            _attrs.putAll(((ErrorCodeException) t)._attrs);
        }
    }

    private void setCode(Throwable e) {
        if (e instanceof ErrorCodeException) {
            setCode(((ErrorCodeException) e).getCode());
        }
    }

    public int getCode() {
        return _code;
    }

    protected void setCode(int code) {
        _code = code;
    }

    public void setAttr(String attrName, String attrValue) {
	    if (attrValue == null) {
		    return;
	    }
        _attrs.put(attrName, attrValue);
    }

    public void setAttr(String attrName, List attrValue) {
	    if (attrValue == null) {
		    return;
	    }
        _attrs.put(attrName, attrValue);
    }

	public void setAttr(String attrName, Set attrValue) {
		setAttr(attrName, new ArrayList(attrValue));
	}

    public void setAttr(String attrName, Map attrValue) {
	    if (attrValue == null) {
		    return;
	    }
        _attrs.put(attrName, attrValue);
    }

    public void setAttr(String name, int value) {
        setAttr(name, Integer.toString(value));
    }
    
    public void setAttr(String name, long value) {
        setAttr(name, Long.toString(value));
    }

    public Element getDOMElement(Document ownerDoc) {
        Element elem = createErrorElement(ownerDoc, getCode(), getMessage(), getAdditionalMessage());
        appendAttributes(elem);
        return elem;
    }

    /**
     * Like {@link #getDOMElement(org.w3c.dom.Document)}
     * @param ownerDoc
     * @return
     */
    private Element createDOMElement(Document ownerDoc, Element parent) {
        Element result = createErrorElement(ownerDoc, getCode(), getOriginalMessage(), getAdditionalMessage());
        appendAttributes(result);
        if (parent != null) {
            parent.appendChild(result);
            Throwable exception = getException();
            if (exception != null) {
                if (exception instanceof ErrorCodeException) {
                    ((ErrorCodeException) exception).createDOMElement(ownerDoc, parent);
                } else {
                    parent.appendChild(createErrorElement(ownerDoc, UNKNOWN_ERROR, exception.getMessage(), null));
                }
            }
        }
        createStackTraceElement(parent);
        return result;
    }

    
	private void updateXmlLocationAttrs(Throwable throwable) {
		// TODO move this logic into Locator class
		if (throwable!=null) {
        	if (throwable instanceof TransformerException) {
        		TransformerException te = (TransformerException)throwable;
        		if (te.getCause() instanceof SAXParseException ) {
        			setSaxParserLocationAttrs((SAXParseException)te.getCause());
        		} else {
        			setTransformerLocationAttrs(te);
        		}
        	} else if (throwable instanceof SAXParseException) {
        		setSaxParserLocationAttrs( (SAXParseException)throwable );
        	}
    	}
	}

	private void setTransformerLocationAttrs(TransformerException te) {
		if (te.getLocator()!=null){
			setLocationAttrs(
					te.getLocator().getLineNumber(), 
					te.getLocator().getColumnNumber(),
					te.getLocator().getPublicId(),
					te.getLocator().getSystemId()
			);
		}
	}

	private void setSaxParserLocationAttrs(SAXParseException se) {
		setLocationAttrs(
				se.getLineNumber(),
				se.getColumnNumber(), 
				se.getPublicId(),
				se.getSystemId()
		);
	}

	private void setLocationAttrs(
			final int lineNumber, final int columnNumber,
			final String publicId, final String systemId) 
	{
		if (lineNumber>=0) {
			setAttr("line", lineNumber);
		}
		if (columnNumber>=0) {
			setAttr("column", columnNumber);
		}
		setAttr("publicId", publicId);
		setAttr("systemId", systemId);
	}

    private void createStackTraceElement(Element result) {
        Document ownerDoc = result.getOwnerDocument();
        Element stackTrace = ownerDoc.createElement("stack-trace");
        StringWriter stringWriter = new StringWriter();
        printStackTrace(new PrintWriter(stringWriter));
        stackTrace.appendChild(ownerDoc.createCDATASection(stringWriter.getBuffer().toString()));
        result.appendChild(stackTrace);
    }

    private void appendAttributes(Element elem) {
        Document ownerDoc = elem.getOwnerDocument();
        for (Iterator i = _attrs.entrySet().iterator(); i.hasNext();) {
            Map.Entry entry = (Map.Entry) i.next();
            Object value = entry.getValue();
            Element param = ownerDoc.createElement("param");
            elem.appendChild(param);
            param.setAttribute("name", entry.getKey().toString());
            if (value instanceof Collection) {
                param.setAttribute("type", "collection");
                appendCollectionValue((Collection) value, param);
            }
            else if (value instanceof Map) {
                param.setAttribute("type", "map");
                appendMapValue((Map) value, param);
            }
            else {
                param.setAttribute("type", "string");
                param.setAttribute("value", value.toString());
            }
        }
    }

    private void appendMapValue(Map value, Element param) {
        Document ownerDoc = param.getOwnerDocument();
        for (Iterator i = value.entrySet().iterator(); i.hasNext();) {
            Map.Entry entry1 = (Map.Entry) i.next();
            Element valueElement = ownerDoc.createElement("value");
            valueElement.setAttribute("id", entry1.getKey().toString());
            param.appendChild(valueElement);
            Object value1 = entry1.getValue();
            if (value1 instanceof Collection) {
                valueElement.setAttribute("type", "collection");
                for (Iterator ii = ((Collection) value1).iterator(); ii.hasNext();) {
                    Object o = ii.next();
                    Element subValueElement = ownerDoc.createElement("subvalue");
                    valueElement.appendChild(subValueElement);
                    subValueElement.appendChild(ownerDoc.createCDATASection(o.toString()));
                }
            }
            else {
                valueElement.setAttribute("type", "string");
                valueElement.appendChild(ownerDoc.createCDATASection(value1.toString()));
            }
        }
    }

    private void appendCollectionValue(Collection value, Element param) {
        Document ownerDoc = param.getOwnerDocument();
        for (Iterator i = value.iterator(); i.hasNext();) {
            Object o = i.next();
            Element valueElement = ownerDoc.createElement("value");
            param.appendChild(valueElement);
            String s;
            if (o instanceof MAttribute) {
                s = ((MAttribute) o).getName();
            }
            else {
                s = o.toString();
            }
            valueElement.appendChild(ownerDoc.createCDATASection(s));
        }
    }

    private Element createErrorElement(Document ownerDoc, int code, String message, String additionalMessage) {
        Element elem = ownerDoc.createElement(ERROR_ELEMENT_NAME);
        elem.setAttribute("code", Integer.toString(code));
        if (ERR_CODES.get(new Integer(code)) != null) {
            elem.setAttribute("id", ERR_CODES.get(new Integer(code)).toString());
        }
        elem.setAttribute("message", message);
        Element m = ownerDoc.createElement("message");
        m.appendChild(ownerDoc.createCDATASection(message));
        elem.appendChild(m);
        if (additionalMessage != null) {
            Element am = ownerDoc.createElement("additional-message");
            am.appendChild(ownerDoc.createCDATASection(additionalMessage));
            elem.appendChild(am);
        }
        return elem;
    }

    public Element toXML(Document document) {
        Element container = document.createElement(ERRORS_ELEMENT_NAME);
        createDOMElement(document, container);
        return container;
    }

    public String getMessage() {
        String message = super.getMessage();
        String strAttrs = (_attrs.isEmpty()) ? "" : " : " + _attrs.toString();
        return message + strAttrs;
    }

    public String toString() {
        return getMessage();
    }

    public void printStackTrace() {
        super.printStackTrace();
        if (getException() != null) {
            System.err.println();
            getException().printStackTrace();
        }
    }

    public void printStackTrace(PrintWriter w) {
        super.printStackTrace(w);
        if (getException() != null) {
            w.println();
            getException().printStackTrace(w);
        }
    }

    public void printStackTrace(PrintStream s) {
        super.printStackTrace(s);
        if (getException() != null) {
            s.println();
            getException().printStackTrace(s);
        }
    }

    private static Map<Integer,String> createCodeToNameMap() {
        HashMap<Integer,String> result = new HashMap<Integer,String>();
        Field[] fields = ADVExceptionCode.class.getFields();
        for (int i = 0; i < fields.length; ++i) {
            Field field = fields[i];
            try {
                if (isPublicStaticFinalInt(field)) {
                    result.put( (Integer)field.get(null), field.getName());
                }
            }
            catch (Exception e) {
                continue;
            }
        }
        return result;
    }

    private static boolean isPublicStaticFinalInt(Field field) {
        return Modifier.isPublic(field.getModifiers()) &&
                Modifier.isStatic(field.getModifiers()) &&
                Modifier.isFinal(field.getModifiers()) &&
                field.getType().getName().equals("int");
    }

	public boolean equals(Object o) {
		if (this == o) return true;
		if (!(o instanceof ErrorCodeException)) return false;

		final ErrorCodeException errorCodeException = (ErrorCodeException) o;

		if (_code != errorCodeException._code) return false;
		if (!_attrs.equals(errorCodeException._attrs)) return false;

		return true;
	}

	public int hashCode() {
		int result;
		result = _code;
		result = 29 * result + _attrs.hashCode();
		return result;
	}

    public Object getAttr(String name) {
        return _attrs.get(name);
    }

    public void addAttrs(Map map) {
        _attrs.putAll(map);
    }

    public void setDatabase(String database) {
        if (database != null) {
            setAttr(DATABASE, database);
        }
    }

    public void setObject(String object) {
        setAttr(OBJECT, object);
    }
    public String getObject() {
        return (String)getAttr(OBJECT);
    }


    public void setAttribute(String attr) {
        setAttr(ATTR, attr);
    }

    public String getAttribute() {
        return (String)getAttr(ATTR);
    }

    public void setTrigger(String triggerId) {
        setAttr(TRIGGER, triggerId);
    }

    public void setLink(String id) {
        setAttr(LINK, id);
    }

    public void setSystemId(String systemId) {
        if (systemId != null) {
            setAttr(SYSTEM_ID, systemId);
        }
    }

    public void setSearchAttrs(Set<?> uniqueAttrs) {
        setAttr(SEARCH_ATTRS, new TreeSet(uniqueAttrs));
    }

}
