package org.xdef.util.xd2xsd;

import org.xdef.XDContainer;
import org.xdef.XDNamedValue;
import org.xdef.XDParser;
import org.xdef.XDValue;
import org.xdef.impl.code.DefContainer;
import org.xdef.impl.code.DefLong;
import org.xdef.impl.code.DefString;
import org.xdef.impl.parsers.XSParseDate;
import org.xdef.impl.parsers.XSParseDatetime;
import org.xdef.impl.parsers.XSParseHexBinary;
import org.xdef.impl.parsers.XSParseList;
import org.xdef.impl.parsers.XSParseString;
import org.xdef.impl.parsers.XSParseTime;
import org.xdef.impl.parsers.XSParseUnion;
import org.xdef.model.XMData;
import org.xdef.msg.XDCONV;
import org.xdef.sys.SException;
import org.xdef.sys.SRuntimeException;

/** Creates from the X-definition parser a parser compatible with XML schema.
 * @author Vaclav Trojan, Anna Kchmascheva
 */
class GenParser {
    private final XDParser _xdp;// XDParser object with parameters (schema type)
    private final String _declaredName;// name of X-definition parser
    private final String _info; // text of information about type conversion
    private String _defaultValue; // default value
    private String _fixedValue; // fixed value

    /** Create new instance of ParserInfo with X-definition parser, declared type name and information text.
     * @param xdp X-definition parser,
     * @param declaredName declared type name in X-definition.
     * @param info information text.
     */
    private GenParser(final XDParser xdp, final String declaredName, final String info) {
        _xdp = xdp;
        if (declaredName != null && !declaredName.isEmpty()) {
            _xdp.setDeclaredName((_declaredName = declaredName));
        } else {
            _declaredName = null;
        }
        _info = info != null && info.isEmpty() ? null : info;
    }

    /** Get X-definition parser.
     * @return X-definition parser.
     */
    protected XDParser getParser() {return _xdp;}

    /** Get information text (used in xs:documentation).
     * @return information text (or null).
     */
    protected String getInfo() {return _info;}

    /** Get name of declared type in X-definition.
     * @return name of declared type in X-definition (or null).
     */
    protected String getDeclaredName() {return _declaredName;}

    /** Get default value from data model.
     * @return default value from data model (or null).
     */
    protected String getDefault() {return _defaultValue;}

    /** Get fixed value from data model.
     * @return fixed value from data model (or null).
     */
    protected String getFixed() {return _fixedValue;}

    /** Create ParserInfo and set to the parser the declared name.
     * @param xdp the X-definition parser.
     * @param info information (i.e. original parser name).
     * @param declaredName name of declared type (or null);.
     * @param xdc Xcontainer with parser parameters.
     * @return ParserInfo object.
     */
    private static GenParser genParserInfo(final XDParser xdp,
        final String info,
        final String declaredName,
        final XDContainer xdc) {
        try {
            GenParser result = new GenParser(xdp,declaredName,info);
            result._xdp.setNamedParams(null, xdc);
            return result;
        } catch (SException ex) {
            throw new SRuntimeException(ex.getMessage(), ex);
        }
    }

    /** Display parameters of validation method from XDContainer
     * @param xdc container with named parameters.
     * @return string with parameter list.
     */
    protected final static String displayParams(final XDContainer xdc) {
        String result = "(";
        for (XDNamedValue x: xdc.getXDNamedItems()) {
            if (result.length() > 1) {
                result += ",";
            }
            result += '%' + x.getName() + '=' + x.getValue();
        }
        return result + ')';
    }

    /** Create ParserInfo object with XML schema compatible parser from XMData.
     * @param xmd XMData object.
     * @param genXdOutFormat if true, from the xdatetime method the outFormat
     * @param genXdateOutFormat if true, use as mask to validate XML data the parameter describing output
     * format from the "xdatetime" method.
     * @return ParserInfo object with XML schema compatible parser.
     */
    protected static GenParser genParser(final XMData xmd, final boolean genXdOutFormat) {
        GenParser result = genParser((XDParser) xmd.getParseMethod(), genXdOutFormat);
        XDValue val = xmd.getFixedValue();
        if (val != null) {
            result._fixedValue = val.toString();
        }
        if ((val = xmd.getDefaultValue()) != null) {
            result._defaultValue = val.toString();
        }
        return result;
    }

    /** Create ParserInfo object with XML schema compatible parser from
     * X-definition parser.
     * @param xdp X-definition parser.
     * @param generator instance of Xd2Xsd.
     * @param genXdateOutFormat if true, use as mask to validate XML data the parameter describing output
     * format from the "xdatetime" method.
     * @return ParserInfo object with XML schema compatible parser.
     */
    private static GenParser genParser(final XDParser xdp, final boolean genXdOutFormat) {
        String name = xdp.parserName();
        String declName = xdp.getDeclaredName();
        XDContainer xdc = xdp.getNamedParams();
        String info = "Created from X-definition " + name;
        String s, mask;
        XDValue x;
        switch(name) {
////////////////////////////////////////////////////////////////////////////////
// XML schema types which are implemented in X-definition
////////////////////////////////////////////////////////////////////////////////
            case "anyURI":
            case "base64Binary":
            case "boolean":
            case "byte":
            case "date":
            case "dateTime":
            case "decimal":
            case "double":
            case "duration":
            case "float":
            case "gDay":
            case "gMonth":
            case "gMonthDay":
            case "gYear":
            case "gYearMonth":
            case "hexBinary":
            case "int":
            case "integer":
            case "language":
            case "long":
            case "Name":
            case "NCName":
            case "negativeInteger":
            case "NMTOKEN":
            case "nonNegativeInteger":
            case "nonPositiveInteger":
            case "normalizedString":
            case "positiveInteger":
            case "short":
            case "string":
            case "time":
            case "token":
            case "unsignedByte":
            case "unsignedInt":
            case "unsignedLong":
            case "unsignedShort": return new GenParser(xdp, declName, null);
            case "list": {
                XSParseList xParseList = (XSParseList) xdp;
                XDParser parser = xParseList.getItemParser();
                xParseList.setItemParser(genParser(parser, genXdOutFormat).getParser());
                return new GenParser(xdp, declName, null);
            }
            case "union": {
                XDParser[] parsers = ((XSParseUnion) xdp).getParsers();
                for (int i = 0; i < parsers.length; i++) {
                    parsers[i] = genParser(parsers[i], genXdOutFormat).getParser();
                }
                return new GenParser(xdp, declName, null);
            }
////////////////////////////////////////////////////////////////////////////////
// X-definition types converted to XML schema type
////////////////////////////////////////////////////////////////////////////////
            case "an":
                info += displayParams(xdc);
                xdc.setXDNamedItem("pattern", new DefString("[A-Za-z0-9]+"));
                return genParserInfo(new XSParseString(), info, declName, xdc);
            case "contains":
                s = xdc.getXDNamedItemAsString("argument");
                xdc.setXDNamedItem("pattern", new DefString(".*" + GenRegex.escapeCharsInString(s) + ".*"));
                xdc.removeXDNamedItem("argument");
                return genParserInfo(new XSParseString(), info + '(' + s + ')', declName, xdc);
            case "containsi":
                s = xdc.getXDNamedItemAsString("argument");
                xdc.removeXDNamedItem("argument");
                mask = GenRegex.genCaseInsensitive(s);
                xdc.setXDNamedItem("pattern", new DefString(".*"+mask+".*"));
                return genParserInfo(new XSParseString(), info + "(" + s + ")", declName, xdc);
            case "country":
                xdc.setXDNamedItem("pattern", new DefString("[a-ZA-Z]{2,3}"));
                return genParserInfo(new XSParseString(), info+"(); see ISO 3166-1 alpha-2,3",declName,xdc);
            case "currency": return genParserInfo(new XSParseString(), "(); see ISO 4217", declName, xdc);
            case "dateYMDhms":
                xdc.setXDNamedItem("pattern", new DefString(
                    "\\d{4}(0[1-9]|1[0-2])(0[1-9]|[1-2]\\d|3[0-1])([0-1]\\d|2[0-3])([0-5]\\d)([0-5]\\d)"));
                return genParserInfo(new XSParseString(), info + "()", declName, xdc);
            case "dec":
                info += "(";
                    mask = "-?\\d+[.,]\\d+";
                if (xdc.getXDNamedItem("totalDigits") != null) {
                    s = xdc.getXDNamedItemAsString("totalDigits");
                    xdc.removeXDNamedItem("totalDigits");
                    info += s;
                    int i = Integer.parseInt(s);
                    if (xdc.getXDNamedItem("fractionDigits") != null) {
                        s = xdc.getXDNamedItemAsString("fractionDigits");
                        xdc.removeXDNamedItem("fractionDigits");
                        info +="," + s;
                        int j = Integer.parseInt(s);
                        mask = (i-j > 1 ? "-?(\\d{0,"+(i-j)+"})" : "\\d") + "([.,]\\d{1,"+j+"})?";
                    } else {
                        xdc.setXDNamedItem("maxLength", new DefString(String.valueOf(i + 1)));
                    }
                }
                info += ")";
                xdc.setXDNamedItem("pattern", new DefString(mask));
                return genParserInfo(new XSParseString(), info, declName, xdc);
            case "domainAddr":
                xdc.setXDNamedItem("pattern", new DefString("[-0-9\\P{Lo}_](\\.[-0-9\\P{Lo}_]){0,99}"));
                return genParserInfo(new XSParseString(), info + "()", declName, xdc);
            case "emailAddr":
                xdc.setXDNamedItem("pattern", new DefString("([ ]*\\([!#$%&'*+/=?0-9\\P{Lo}_ .-]*\\)[ ]*)*"
                    +"(([!#$%&'*+/=?0-9\\P{Lo}_ .-]*"
                    + "<([!#$%&'*+/=?0-9\\P{Lo}-]+(\\.[!#$%&'*+/=?0-9\\P{Lo}_-]*)*)"
                    + "@([0-9\\P{Lo}_-]+(\\.[0-9\\P{Lo}_-]*)*>))"
                    + "|(([!#$%&'*+/=?0-9\\P{Lo}_-]+(\\.[!#$%&'*+/=?0-9\\P{Lo}_-]*)*)"
                    + "@([0-9\\P{Lo}_-]+(\\.[0-9\\P{Lo}_-]*)*)))"
                    + "([ ]*\\([0-9\\P{Lo}_ .-]*\\))?"));
                return genParserInfo(new XSParseString(), info + "()", declName, xdc);
            case "emailDate":
                xdc.setXDNamedItem("pattern", new DefContainer(GenRegex.getRegexes(
                    "[EEE, ]d MMM yyyy HH:mm[:ss][ ZZZZZ][ (z)]|[EEE, ]d MMM YY HH:mm[:ss][ ZZZZZ][ (z)]")));
                return genParserInfo(new XSParseString(), info, declName, xdc);
            case "ends":
                s = xdc.getXDNamedItemAsString("argument");
                xdc.setXDNamedItem("pattern", new DefString(".*" + GenRegex.escapeCharsInString(s)));
                xdc.removeXDNamedItem("argument");
                return genParserInfo(new XSParseString(), info + "(" + s + ")", declName, xdc);
            case "endsi":
                s = xdc.getXDNamedItemAsString("argument");
                xdc.removeXDNamedItem("argument");
                mask = GenRegex.genCaseInsensitive(s);
                xdc.setXDNamedItem("pattern", new DefString(".*" + mask));
                return genParserInfo(new XSParseString(), info + '(' + s + ')', declName, xdc);
            case "enum":
                x = xdc.getXDNamedItem("argument").getValue();
                xdc.setXDNamedItem("enumeration", x);
                xdc.removeXDNamedItem("argument");
                s = "";
                if (x instanceof XDContainer) {
                    XDContainer y = (XDContainer) x;
                    for (int i=0; i < y.getXDItemsNumber(); i++) {
                        if (i > 0) s += ',';
                        s += "" + y.getXDItem(i);
                    }
                } else {
                    s += "" + x;
                }
                return genParserInfo(new XSParseString(), info + '(' + s + ')', declName, xdc);
            case "eq":
                s = xdc.getXDNamedItemAsString("argument");
                xdc.setXDNamedItem("enumeration", new DefString(s));
                xdc.removeXDNamedItem("argument");
                return genParserInfo(new XSParseString(), info + "(" + s + ")", declName, xdc);
            case "eqi":
                s = xdc.getXDNamedItemAsString("argument");
                xdc.removeXDNamedItem("argument");
                mask = GenRegex.genCaseInsensitive(s);
                xdc.setXDNamedItem("pattern", new DefString(mask));
                return genParserInfo(new XSParseString(), info + "(" + s + ")", declName, xdc);
            case "hex":
                return genParserInfo(new XSParseHexBinary(),info,declName,xdc);
            case "ipAddr":
                s = "((25[0-5])|(2[0-4][0-9])|([0-1]?[0-9][0-9])|([0-9]))";
                mask = "([0-9a-fA-F]{1,4}(:[0-9a-fA-F]{1,4}){1,7})|(" + s + "(\\." + s + "){3})";
                xdc.setXDNamedItem("pattern", new DefString(mask));
                return genParserInfo(new XSParseString(), info, declName, xdc);
            case "letters":
                info += displayParams(xdc);
                xdc.setXDNamedItem("pattern", new DefString("[A-Za-z]+"));
                return genParserInfo(new XSParseString(), info, declName, xdc);
            case "MD5":
                xdc.setXDNamedItem("length", new DefLong(16));
                return genParserInfo(new XSParseHexBinary(), info + displayParams(xdc), declName, xdc);
            case "num":
                info += '(';
                if ((s = xdc.getXDNamedItemAsString("length")) != null) {
                    xdc.removeXDNamedItem("length");
                    mask = "{" + s + "}";
                    info += s;
                } else if ((s = xdc.getXDNamedItemAsString("minLength"))!=null){
                    xdc.removeXDNamedItem("minLength");
                    mask = "{" + s;
                    info += s;
                    s = "," + xdc.getXDNamedItemAsString("maxLength");
                    xdc.removeXDNamedItem("maxLength");
                    mask += s +  "}";
                    info += s;
                } else {
                    mask = "+";
                }
                info += ")";
                xdc.setXDNamedItem("pattern", new DefString("\\d" + mask));
                return genParserInfo(new XSParseString(), info, declName, xdc);
            case "pic":
                s = xdc.getXDNamedItemAsString("argument");
                xdc.removeXDNamedItem("argument");
                mask = "";
                for (char c: s.toCharArray()) {
                    switch (c) {
                        case '9': mask += "\\d"; continue;
                        case 'A': mask += "[a-zA-Z]"; continue;
                        case 'X': mask += "[0-9a-zA-Z]"; continue;
                    }
                    mask += GenRegex.createEscapedChar(c);
                }
                xdc.setXDNamedItem("pattern", new DefString(mask));
                return genParserInfo(new XSParseString(), info + "(" + s + ")", declName, xdc);
            case "price":
                xdc.setXDNamedItem("pattern", new DefString("(0|[1-9]\\d*)(\\.\\d+)?[ ][A-Z]{3}"));
                return genParserInfo(new XSParseString(), info + "()", declName, xdc);
            case "printableDate":
                xdc.setXDNamedItem("pattern", new DefContainer(GenRegex.getRegexes(
                    "[EEE ]MMM d HH:mm[:ss[.S]][ z] y|({L(*)}[EEE ]MMM d HH:mm[:ss[.S]][ z] y)")));
                return genParserInfo(new XSParseString(), info, declName, xdc);
            case "regex":
                s = xdc.getXDNamedItemAsString("argument");
                xdc.removeXDNamedItem("argument");
                xdc.setXDNamedItem("pattern", new DefString(s));
                return genParserInfo(new XSParseString(), info + "(" + s + ")", declName, xdc);
            case "starts":
                s = xdc.getXDNamedItemAsString("argument");
                xdc.removeXDNamedItem("argument");
                xdc.setXDNamedItem("pattern", new DefString(GenRegex.escapeCharsInString(s)+"*"));
                return genParserInfo(new XSParseString(), info + "(" + s + ")", declName, xdc);
            case "startsi":
                s = xdc.getXDNamedItemAsString("argument");
                xdc.removeXDNamedItem("argument");
                mask = GenRegex.genCaseInsensitive(s);
                xdc.setXDNamedItem("pattern", new DefString(mask + "*"));
                return genParserInfo(new XSParseString(), info + "(" + s + ")", declName, xdc);
            case "SHA1":
                xdc.setXDNamedItem("length", new DefLong(20));
                return genParserInfo(new XSParseHexBinary(), info + displayParams(xdc), declName, xdc);
            case "telephone":
                xdc.setXDNamedItem("pattern", new DefString("([+]\\d+[ ])?(\\d+([ ]\\d+)*)"));
                return genParserInfo(new XSParseString(), info, declName, xdc);
            case "xdatetime":
                mask = genXdOutFormat ? xdc.getXDNamedItemAsString("outFormat") : null;
                if (mask == null || mask.isEmpty()) {
                    mask = xdc.getXDNamedItemAsString("format");
                    if (mask == null || mask.isEmpty()) {
                        //Missing parameters of method "xdatetime"
                        throw new SRuntimeException(XDCONV.XDCONV208);
                    }
                }
                info += '(' + mask;
                xdc.removeXDNamedItem("outFormat");
                xdc.removeXDNamedItem("format");
                for (XDNamedValue xv: xdc.getXDNamedItems()) {
                    info += ",%" + xv.getName() + '=' + xv.getValue();
                }
                info += ')';
                switch (mask) {
                    case "yyyy-MM-dd":
                    case "yyyy-MM-ddZ": return genParserInfo(new XSParseDate(), info, declName, xdc);
                    case "HH:mm:ss":
                    case "HH:mm:ssZ":
                    case "HH:mm:ss.S":
                    case "HH:mm:ss.SZ": return genParserInfo(new XSParseTime(), info, declName, xdc);
                    case "yyyy-MM-ddTHH:mm:ss":
                    case "yyyy-MM-ddTHH:mm:ssZ":
                    case "yyyy-MM-ddTHH:mm:ss.S":
                    case "yyyy-MM-ddTHH:mm:ss.SZ":
                        return genParserInfo(new XSParseDatetime(), info, declName, xdc);
                    default: // return string type with patterns
                        xdc.setXDNamedItem("pattern", new DefContainer(GenRegex.getRegexes(mask)));
                        return genParserInfo(new XSParseString(), info, declName, xdc);
                } // switch mask
////////////////////////////////////////////////////////////////////////////////
// other X-definition types are converted to xs:string
////////////////////////////////////////////////////////////////////////////////
            default:
                // remove xdef parameters
                xdc.removeXDNamedItem("a1");
                xdc.removeXDNamedItem("a2");
                xdc.removeXDNamedItem("argument");
                xdc.removeXDNamedItem("outFormat");
                xdc.removeXDNamedItem("format");
                info += displayParams(xdc);
                return genParserInfo(new XSParseString(), info, declName, new DefContainer());
        }
    }
}