/*
 * See LICENSE file in distribution for copyright and licensing information.
 */
package org.jvyamlb;

import java.io.FileInputStream;
import java.io.InputStream;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.jvyamlb.exceptions.ConstructorException;
import org.jvyamlb.exceptions.YAMLException;

import org.jvyamlb.nodes.Node;

import org.jvyamlb.util.Base64Coder;

import org.jruby.util.ByteList;

import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;

/**
 * @author <a href="mailto:ola.bini@gmail.com">Ola Bini</a>
 */
public class SafeConstructorImpl extends BaseConstructorImpl {
    private final static Map yamlConstructors = new HashMap();
    private final static Map yamlMultiConstructors = new HashMap();
    private final static Map yamlMultiRegexps = new HashMap();
    public YamlConstructor getYamlConstructor(final Object key) {
        YamlConstructor mine = (YamlConstructor)yamlConstructors.get(key);
        if(mine == null) {
            mine = super.getYamlConstructor(key);
        }
        return mine;
    }

    public YamlMultiConstructor getYamlMultiConstructor(final Object key) {
        YamlMultiConstructor mine = (YamlMultiConstructor)yamlMultiConstructors.get(key);
        if(mine == null) {
            mine = super.getYamlMultiConstructor(key);
        }
        return mine;
    }

    public Pattern getYamlMultiRegexp(final Object key) {
        Pattern mine = (Pattern)yamlMultiRegexps.get(key);
        if(mine == null) {
            mine = super.getYamlMultiRegexp(key);
        }
        return mine;
    }

    public Set getYamlMultiRegexps() {
        final Set all = new HashSet(super.getYamlMultiRegexps());
        all.addAll(yamlMultiRegexps.keySet());
        return all;
    }

    public static void addConstructor(final String tag, final YamlConstructor ctor) {
        yamlConstructors.put(tag,ctor);
    }

    public static void addMultiConstructor(final String tagPrefix, final YamlMultiConstructor ctor) {
        yamlMultiConstructors.put(tagPrefix,ctor);
        yamlMultiRegexps.put(tagPrefix,Pattern.compile("^"+tagPrefix));
    }

    public SafeConstructorImpl(final Composer composer) {
        super(composer);
    }

    private static ByteList into(String v) {
        return new ByteList(v.getBytes(),false);
    }

    private final static Map BOOL_VALUES = new HashMap();
    static {
        BOOL_VALUES.put(into("yes"),Boolean.TRUE);
        BOOL_VALUES.put(into("Yes"),Boolean.TRUE);
        BOOL_VALUES.put(into("YES"),Boolean.TRUE);
        BOOL_VALUES.put(into("no"),Boolean.FALSE);
        BOOL_VALUES.put(into("No"),Boolean.FALSE);
        BOOL_VALUES.put(into("NO"),Boolean.FALSE);
        BOOL_VALUES.put(into("true"),Boolean.TRUE);
        BOOL_VALUES.put(into("True"),Boolean.TRUE);
        BOOL_VALUES.put(into("TRUE"),Boolean.TRUE);
        BOOL_VALUES.put(into("false"),Boolean.FALSE);
        BOOL_VALUES.put(into("False"),Boolean.FALSE);
        BOOL_VALUES.put(into("FALSE"),Boolean.FALSE);
        BOOL_VALUES.put(into("on"),Boolean.TRUE);
        BOOL_VALUES.put(into("On"),Boolean.TRUE);
        BOOL_VALUES.put(into("ON"),Boolean.TRUE);
        BOOL_VALUES.put(into("off"),Boolean.FALSE);
        BOOL_VALUES.put(into("Off"),Boolean.FALSE);
        BOOL_VALUES.put(into("OFF"),Boolean.FALSE);
    }

    public static Object constructYamlNull(final Constructor ctor, final Node node) {
        return null;
    }
    
    public static Object constructYamlBool(final Constructor ctor, final Node node) {
        final Object val = ctor.constructScalar(node);
        return BOOL_VALUES.get(val);
    }

    public static Object constructYamlOmap(final Constructor ctor, final Node node) {
        return ctor.constructPairs(node);
    }

    public static Object constructYamlPairs(final Constructor ctor, final Node node) {
        return constructYamlOmap(ctor,node);
    }

    public static Object constructYamlSet(final Constructor ctor, final Node node) {
        return ((Map)ctor.constructMapping(node)).keySet();
    }

    public static Object constructYamlStr(final Constructor ctor, final Node node) {
        final ByteList value = (ByteList)ctor.constructScalar(node);
        return value.length() == 0 ? (ByteList)null : value;
    }

    public static Object constructYamlSeq(final Constructor ctor, final Node node) {
        return ctor.constructSequence(node);
    }

    public static Object constructYamlMap(final Constructor ctor, final Node node) {
        return ctor.constructMapping(node);
    }

    public static Object constructUndefined(final Constructor ctor, final Node node) {
        throw new ConstructorException(null,"could not determine a constructor for the tag " + node.getTag(),null);
    }

    private final static Pattern TIMESTAMP_REGEXP = Pattern.compile("^(-?[0-9][0-9][0-9][0-9])-([0-9][0-9]?)-([0-9][0-9]?)(?:(?:[Tt]|[ \t]+)([0-9][0-9]?):([0-9][0-9]):([0-9][0-9])(?:\\.([0-9]*))?(?:[ \t]*(Z|([-+][0-9][0-9]?)(?::([0-9][0-9])?)?)))?$");
    public final static Pattern YMD_REGEXP = Pattern.compile("^(-?[0-9][0-9][0-9][0-9])-([0-9][0-9]?)-([0-9][0-9]?)$");
    public static Object constructYamlTimestamp(final Constructor ctor, final Node node) {
        Matcher match = YMD_REGEXP.matcher(node.getValue().toString());
        if(match.matches()) {
            final String year_s = match.group(1);
            final String month_s = match.group(2);
            final String day_s = match.group(3);
            DateTime dt = new DateTime(0,1,1,0,0,0,0);
            if(year_s != null) {
                dt = dt.withYear(Integer.parseInt(year_s));
            }
            if(month_s != null) {
                dt = dt.withMonthOfYear(Integer.parseInt(month_s));
            }
            if(day_s != null) {
                dt = dt.withDayOfMonth(Integer.parseInt(day_s));
            }
            return new Object[]{dt};
        }
        match = TIMESTAMP_REGEXP.matcher(node.getValue().toString());
        if(!match.matches()) {
            return new Object[]{ctor.constructPrivateType(node)};
        }
        final String year_s = match.group(1);
        final String month_s = match.group(2);
        final String day_s = match.group(3);
        final String hour_s = match.group(4);
        final String min_s = match.group(5);
        final String sec_s = match.group(6);
        final String fract_s = match.group(7);
        final String utc = match.group(8);
        final String timezoneh_s = match.group(9);
        final String timezonem_s = match.group(10);

        int usec = 0;
        if(fract_s != null) {
            usec = Integer.parseInt(fract_s);
            if(usec != 0) {
                while(10*usec < 1000) {
                    usec *= 10;
                }
            }
        }
        DateTime dt = new DateTime(0,1,1,0,0,0,0);
        if("Z".equalsIgnoreCase(utc)) {
            dt = dt.withZone(DateTimeZone.forID("Etc/GMT"));
        } else {
            if(timezoneh_s != null || timezonem_s != null) {
                int zone = 0;
                int sign = +1;
                if(timezoneh_s != null) {
                    if(timezoneh_s.startsWith("-")) {
                        sign = -1;
                    }
                    zone += Integer.parseInt(timezoneh_s.substring(1))*3600000;
                }
                if(timezonem_s != null) {
                    zone += Integer.parseInt(timezonem_s)*60000;
                }
                dt = dt.withZone(DateTimeZone.forOffsetMillis(sign*zone));
            }
        }
        if(year_s != null) {
            dt = dt.withYear(Integer.parseInt(year_s));
        }
        if(month_s != null) {
            dt = dt.withMonthOfYear(Integer.parseInt(month_s));
        }
        if(day_s != null) {
            dt = dt.withDayOfMonth(Integer.parseInt(day_s));
        }
        if(hour_s != null) {
            dt = dt.withHourOfDay(Integer.parseInt(hour_s));
        }
        if(min_s != null) {
            dt = dt.withMinuteOfHour(Integer.parseInt(min_s));
        }
        if(sec_s != null) {
            dt = dt.withSecondOfMinute(Integer.parseInt(sec_s));
        }
        dt = dt.withMillisOfSecond(usec/1000);

        return new Object[]{dt, new Integer(usec%1000)};
    }

    public static Object constructYamlInt(final Constructor ctor, final Node node) {
        String value = ctor.constructScalar(node).toString().replaceAll("_","").replaceAll(",","");;
        int sign = +1;
        char first = value.charAt(0);
        if(first == '-') {
            sign = -1;
            value = value.substring(1);
        } else if(first == '+') {
            value = value.substring(1);
        }
        int base = 10;
        if(value.equals("0")) {
            return new Long(0);
        } else if(value.startsWith("0b")) {
            value = value.substring(2);
            base = 2;
        } else if(value.startsWith("0x")) {
            value = value.substring(2);
            base = 16;
        } else if(value.startsWith("0")) {
            value = value.substring(1);
            base = 8;
        } else if(value.indexOf(':') != -1) {
            final String[] digits = value.split(":");
            int bes = 1;
            int val = 0;
            for(int i=0,j=digits.length;i<j;i++) {
                val += (Long.parseLong(digits[(j-i)-1])*bes);
                bes *= 60;
            }
            return new Integer(sign*val);
        } else {
            try {
                return new Long(sign * Long.parseLong(value));
            } catch(Exception e) {
                if(sign < 0) {
                    return new java.math.BigInteger(value).negate();
                } else {
                    return new java.math.BigInteger(value);
                }
            }
        }
        try {
            return new Long(sign * Long.parseLong(value,base));
        } catch(Exception e) {
            if(sign < 0) {
                return new java.math.BigInteger(value,base).negate();
            } else {
                return new java.math.BigInteger(value,base);
            }
        }
    }

    private final static Double INF_VALUE_POS = new Double(Double.POSITIVE_INFINITY);
    private final static Double INF_VALUE_NEG = new Double(Double.NEGATIVE_INFINITY);
    private final static Double NAN_VALUE = new Double(Double.NaN);

    public static Object constructYamlFloat(final Constructor ctor, final Node node) {
        String value = ctor.constructScalar(node).toString().replaceAll("_","").replaceAll(",","");
        int sign = +1;
        char first = value.charAt(0);
        if(first == '-') {
            sign = -1;
            value = value.substring(1);
        } else if(first == '+') {
            value = value.substring(1);
        }
        final String valLower = value.toLowerCase();
        if(valLower.equals(".inf")) {
            return sign == -1 ? INF_VALUE_NEG : INF_VALUE_POS;
        } else if(valLower.equals(".nan")) {
            return NAN_VALUE;
        } else if(value.indexOf(':') != -1) {
            final String[] digits = value.split(":");
            int bes = 1;
            double val = 0.0;
            for(int i=0,j=digits.length;i<j;i++) {
                val += (Double.parseDouble(digits[(j-i)-1])*bes);
                bes *= 60;
            }
            return new Double(sign*val);
        } else {
            return new Double(sign*Double.parseDouble(value));
        }
    }    

    public static Object constructYamlBinary(final Constructor ctor, final Node node) {
        final String[] values = ctor.constructScalar(node).toString().split("\n|(?:\r[^\n])");
        final StringBuffer vals = new StringBuffer();
        for(int i=0,j=values.length;i<j;i++) {
            vals.append(values[i].trim());
        }
        return Base64Coder.decode(ByteList.plain(vals));
    }

    public static Object constructSpecializedSequence(final Constructor ctor, final String pref, final Node node) {
        List outp = null;
        try {
            final Class seqClass = Class.forName(pref);
            outp = (List)seqClass.newInstance();
        } catch(final Exception e) {
            throw new YAMLException("Can't construct a sequence from class " + pref + ": " + e.toString());
        }
        outp.addAll((List)ctor.constructSequence(node));
        return outp;
    }

    public static Object constructSpecializedMap(final Constructor ctor, final String pref, final Node node) {
        Map outp = null;
        try {
            final Class mapClass = Class.forName(pref);
            outp = (Map)mapClass.newInstance();
        } catch(final Exception e) {
            throw new YAMLException("Can't construct a mapping from class " + pref + ": " + e.toString());
        }
        outp.putAll((Map)ctor.constructMapping(node));
        return outp;
    }

    private static Object fixValue(final Object inp, final Class outp) {
        if(inp == null) {
            return null;
        }
        final Class inClass = inp.getClass();
        if(outp.isAssignableFrom(inClass)) {
            return inp;
        }
        if(inClass == Long.class && (outp == Integer.class || outp == Integer.TYPE)) {
            return new Integer(((Long)inp).intValue());
        }
        if(inClass == Long.class && (outp == Short.class || outp == Short.TYPE)) {
            return new Short((short)((Long)inp).intValue());
        }
        if(inClass == Long.class && (outp == Character.class || outp == Character.TYPE)) {
            return new Character((char)((Long)inp).intValue());
        }
        if(inClass == Double.class && (outp == Float.class || outp == Float.TYPE)) {
            return new Float(((Double)inp).floatValue());
        }
        return inp;
    }

    public static Object constructJava(final Constructor ctor, final String pref, final Node node) {
        Object outp = null;
        try {
            final Class cl = Class.forName(pref);
            outp = cl.newInstance();
            final Map values = (Map)ctor.constructMapping(node);
            java.lang.reflect.Method[] ems = cl.getMethods();
            for(final Iterator iter = values.keySet().iterator();iter.hasNext();) {
                final Object key = iter.next();
                final Object value = values.get(key);
                final String keyName = key.toString();
                final String mName = "set" + Character.toUpperCase(keyName.charAt(0)) + keyName.substring(1);
                for(int i=0,j=ems.length;i<j;i++) {
                    if(ems[i].getName().equals(mName) && ems[i].getParameterTypes().length == 1) {
                        ems[i].invoke(outp, new Object[]{fixValue(value, ems[i].getParameterTypes()[0])});
                        break;
                    }
                }
            }
        } catch(final Exception e) {
            throw new YAMLException("Can't construct a java object from class " + pref + ": " + e.toString());
        }
        return outp;
    }

    static {
        BaseConstructorImpl.addConstructor("tag:yaml.org,2002:null",new YamlConstructor() {
                public Object call(final Constructor self, final Node node) {
                    return constructYamlNull(self,node);
                }
            });
        addConstructor("tag:yaml.org,2002:bool",new YamlConstructor() {
                public Object call(final Constructor self, final Node node) {
                    return constructYamlBool(self,node);
                }
            });
        addConstructor("tag:yaml.org,2002:omap",new YamlConstructor() {
                public Object call(final Constructor self, final Node node) {
                    return constructYamlOmap(self,node);
                }
            });
        addConstructor("tag:yaml.org,2002:pairs",new YamlConstructor() {
                public Object call(final Constructor self, final Node node) {
                    return constructYamlPairs(self,node);
                }
            });
        addConstructor("tag:yaml.org,2002:set",new YamlConstructor() {
                public Object call(final Constructor self, final Node node) {
                    return constructYamlSet(self,node);
                }
            });
        addConstructor("tag:yaml.org,2002:int",new YamlConstructor() {
                public Object call(final Constructor self, final Node node) {
                    return constructYamlInt(self,node);
                }
            });
        addConstructor("tag:yaml.org,2002:float",new YamlConstructor() {
                public Object call(final Constructor self, final Node node) {
                    return constructYamlFloat(self,node);
                }
            });
        addConstructor("tag:yaml.org,2002:timestamp",new YamlConstructor() {
                public Object call(final Constructor self, final Node node) {
                    return ((Object[])constructYamlTimestamp(self,node))[0];
                }
            });
        addConstructor("tag:yaml.org,2002:timestamp#ymd",new YamlConstructor() {
                public Object call(final Constructor self, final Node node) {
                    return ((Object[])constructYamlTimestamp(self,node))[0];
                }
            });
        addConstructor("tag:yaml.org,2002:str",new YamlConstructor() {
                public Object call(final Constructor self, final Node node) {
                    return constructYamlStr(self,node);
                }
            });
        addConstructor("tag:yaml.org,2002:binary",new YamlConstructor() {
                public Object call(final Constructor self, final Node node) {
                    return constructYamlBinary(self,node);
                }
            });
        addConstructor("tag:yaml.org,2002:seq",new YamlConstructor() {
                public Object call(final Constructor self, final Node node) {
                    return constructYamlSeq(self,node);
                }
            });
        addConstructor("tag:yaml.org,2002:map",new YamlConstructor() {
                public Object call(final Constructor self, final Node node) {
                    return constructYamlMap(self,node);
                }
            });
        addConstructor(null,new YamlConstructor() {
                public Object call(final Constructor self, final Node node) {
                    return self.constructPrivateType(node);
                }
            });

        addMultiConstructor("tag:yaml.org,2002:seq:",new YamlMultiConstructor() {
                public Object call(final Constructor self, final String pref, final Node node) {
                    return constructSpecializedSequence(self,pref,node);
                }
            });
        addMultiConstructor("tag:yaml.org,2002:map:",new YamlMultiConstructor() {
                public Object call(final Constructor self, final String pref, final Node node) {
                    return constructSpecializedMap(self,pref,node);
                }
            });
        addMultiConstructor("!java/object:",new YamlMultiConstructor() {
                public Object call(final Constructor self, final String pref, final Node node) {
                    return constructJava(self,pref,node);
                }
            });
        addMultiConstructor("tag:java.yaml.org,2002:object:",new YamlMultiConstructor() {
                public Object call(final Constructor self, final String pref, final Node node) {
                    return constructJava(self,pref,node);
                }
            });
    }

    public static void main(final String[] args) throws Exception {
        final String filename = args[0];
        System.out.println("Reading of file: \"" + filename + "\"");

        final ByteList input = new ByteList(1024);
        final InputStream reader = new FileInputStream(filename);
        byte[] buff = new byte[1024];
        int read = 0;
        while(true) {
            read = reader.read(buff);
            input.append(buff,0,read);
            if(read < 1024) {
                break;
            }
        }
        reader.close();
        final long before = System.currentTimeMillis();
        for(int i=0;i<1;i++) {
            final Constructor ctor = new SafeConstructorImpl(new ComposerImpl(new ParserImpl(new ScannerImpl(input)),new ResolverImpl()));
            for(final Iterator iter = ctor.eachDocument();iter.hasNext();) {
                iter.next();
                //System.out.println(iter.next());
            }
        }
        final long after = System.currentTimeMillis();
        final long time = after-before;
        final double timeS = (after-before)/1000.0;
        System.out.println("Walking through the nodes for the file: " + filename + " took " + time + "ms, or " + timeS + " seconds"); 
    }
}// SafeConstructorImpl
