package usda.weru.weps.location;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.measure.Measurable;
import javax.measure.Measure;
import javax.measure.quantity.Duration;
import javax.measure.unit.NonSI;
import javax.measure.unit.SI;
import org.apache.log4j.Logger;
import org.jscience.geography.coordinates.LatLong;
import usda.weru.gis.GISUtil;
import usda.weru.util.Caster;

/**
 *
 * @param <S> the type of the smaller subdivisions
 * @author Joseph Levin <joelevin@weru.ksu.edu>
 */
public abstract class Site<S extends Site<?>> implements Comparable<Site<?>> {

    private static final Logger LOGGER = Logger.getLogger(Site.class);
    private static final Map<String, Site.Level0> COUNTRIES_FIPS_CHAR2;
    private static final Map<String, Site<?>> SITES_FIPS_CHAR4;
    private static final Map<String, Site<?>> SITES_HASC;

    /**
     *
     */
    protected String c_displayName;
    private final Designation c_type;

    /**
     *
     */
    protected String c_abbr;

    /**
     *
     */
    protected LatLong c_latlongFallback;

    /**
     *
     */
    protected LatLong c_latlong;

    /**
     * only needs to be unique to the parent
     */
    protected String c_primaryKey;

    /**
     * Tree
     */
    protected Site<?> c_parent;

    /**
     *
     */
    protected List<S> c_divisions;

    /**
     *
     */
    protected Map<String, Site<?>> c_divisonAbbr;

    /**
     *
     */
    protected String c_fips;
    
    /**
     *
     */
    protected String c_fipscode;
    //@Deprecated

    /**
     *
     */
    protected String c_hasc;

    private Site(Designation type) {
        c_type = type;
    }

    /**
     *
     * @return
     */
    public Designation getDesgination() {
        return c_type;
    }

    /**
     *
     * @return
     */
    public String getDisplayName() {
        return c_displayName;
    }

    /**
     *
     * @return
     */
    public LatLong getLatLong() {
        if (c_latlong == null) {
            LOGGER.debug("Querying gis data for centroid of " + this + ".");
            LatLong centroid = GISUtil.representativeLatLong(this);
            if (centroid != null) {
                LOGGER.debug("Found centroid of " + this + ".  Centroid=" + centroid);
                c_latlong = centroid;
            } else if (c_latlongFallback != null) {
                LOGGER.debug("Unable to determine centroid for " + this + ". Using fallback latlong.");
                return c_latlongFallback;
            } else if (getParent() != null) {
                LOGGER.debug("Unable to determine centroid for " + this + ". Using parent latlong.");
                return getParent().getLatLong();
            } else {
                LOGGER.warn("Unable to determine centroid for " + this + ".");
                return null;
            }
        }
        return c_latlong;
    }

    /**
     *
     * @return
     */
    public Site<?> getParent() {
        return c_parent;
    }

    /**
     *
     * @return
     */
    public String getAbbreviation() {
        return c_abbr;
    }

    /**
     *
     * @return
     */
    public String getPrimaryKey() {
        return c_primaryKey;
    }

    /**
     *
     * @return
     */
    public String getFIPS() {
        return c_fips;
    }
    
    /**
     * 
     * @return the FIPS code
     */
    public String getFIPSCode()
    {
        return c_fipscode;
    }
    
    /**
     * Sets the FIPS String
     */
    public void setFIPSCode(String input)
    {
        c_fipscode = input;
    }

    //@Deprecated
    /**
     *
     * @return
     */
    public String getHASC() {
        return c_hasc;
    }

    /**
     *
     * @return
     */
    public abstract S[] getSubDivisions();

    /**
     *
     * @return
     */
    protected synchronized List<S> getSubDivisionList() {
        if (c_divisions == null) {
            c_divisions = new ArrayList<S>();
        }
        return c_divisions;
    }

    /**
     *
     * @param primaryKey
     * @return
     */
    public S getSubDivision(String primaryKey) {
        for (S site : getSubDivisionList()) {
            if (primaryKey.equals(site.getPrimaryKey())) {
                return site;
            }
        }
        return null;
    }

    /**
     *
     * @return
     */
    @Override
    public String toString() {

        return (c_parent != null ? c_parent.toString() + "-" : "") + c_primaryKey;
    }

    /**
     *
     * @return
     */
    public static Site.Level0[] countries() {
        Collection<Site.Level0> temp = COUNTRIES_FIPS_CHAR2.values();
        return temp.toArray(new Site.Level0[temp.size()]);
    }

    /**
     *
     * @param o
     * @return
     */
    @Override
    public int compareTo(Site<?> o) {
        String name = o != null ? o.getDisplayName() : null;
        return getDisplayName().compareTo(name);
    }

    /**
     *
     * @param fips
     * @return
     * @throws IllegalArgumentException
     */
    public static Site<?> valueOfFIPS(String fips) throws IllegalArgumentException {
        fips = fips.trim();
        if (fips.length() != 4) {
            throw new IllegalArgumentException("A FIPS code must be 4 characters long.");
        }
        return Site.SITES_FIPS_CHAR4.get(fips);
    }

    /**
     *
     * @param hasc
     * @return
     * @throws IllegalArgumentException
     */
    public static Site<?> valueOfHASC(String hasc) throws IllegalArgumentException {
        if (hasc != null) {
            hasc = hasc.trim();
            return !hasc.isEmpty() ? SITES_HASC.get(hasc) : null;
        }

        return null;
    }

    /**
     *
     * @param value
     * @return
     */
    public static Site<?> valueOf(String value) {
        if (value == null) {
            return null;
        }
        value = value.trim();
        String[] parts = value.split("\\-");

        Site<?> temp = null;
        for (int i = 0; i < parts.length; i++) {
            if (i == 0) {
                //TODO: add support for iso codes?
                if (parts[i].toUpperCase().startsWith("FIPS:")) {
                    String code = parts[i].substring(5);
                    if (code.length() == 2) {
                        code = code + "00";
                    }
                    temp = Site.valueOfFIPS(code);

                    if (temp == null) {
                        LOGGER.error("Unknown country : " + parts[i]);
                        return null;
                    }
                } else {
                    LOGGER.debug("Unknown country code.  Should start with FIPS: " + parts[i]);
                    return null;
                }
            } else {
/**
                 * Note:  Assertions are not enabled.  These will be useless items
                 * unless assertions are enabled.  Thus, they will be commented out unless
                 * the user wishes to enable specific assertions (feed the virtual machine 
                 * the -ea argument).
                 */
//                assert (temp != null);
                Site<?> temp2 = temp.getSubDivision(parts[i]);
                if (temp2 != null) {
                    temp = temp2;
                } else {
                    LOGGER.error("Unknown sub division of " + temp.toString() + ": " + parts[i]);
                    return temp;
                }
            }
        }
        return temp;
    }

    /**
     *
     */
    public static class Level0 extends Site<Level1> {

        private String c_fipsChar4;
        private String c_iso3166Char2;
        private String c_iso3166Char3;
        private int c_iso3166Number;

        private Level0(Designation type, String fips) {
            super(type);
            c_fipsChar4 = fips;
        }

        /**
         *
         * @return
         */
        @Override
        public String getPrimaryKey() {
            return getFIPSChar2();
        }

        /**
         *
         * @return
         */
        public String getFIPSChar4() {
            return c_fipsChar4;
        }

        /**
         *
         * @return
         */
        public String getFIPSChar2() {
            return c_fipsChar4.substring(0, 2);
        }

        /**
         *
         * @return
         */
        public String getISO3166Char2() {
            return c_iso3166Char2;
        }

        /**
         *
         * @return
         */
        public String getISO3166Char3() {
            return c_iso3166Char3;
        }

        /**
         *
         * @return
         */
        public int getISO3166Number() {
            return c_iso3166Number;
        }

        /**
         *
         * @return
         */
        @Override
        public String getAbbreviation() {
            if (super.getAbbreviation() == null) {
                return super.getAbbreviation();
            } else {
                return getFIPSChar2();
            }
        }

        /**
         *
         * @return
         */
        @Override
        public String toString() {
            return "FIPS:" + getFIPSChar2();
        }

        /**
         *
         * @return
         */
        @Override
        public Level1[] getSubDivisions() {
            return c_divisions != null ? c_divisions.toArray(new Level1[c_divisions.size()]) : new Level1[0];
        }
    }

    /**
     *
     */
    public static class Level1 extends Site<Level2> {

        /**
         *
         * @param parent
         * @param type
         */
        public Level1(Level0 parent, Designation type) {
            super(type);
            c_parent = parent;
            c_fipscode = parent.getFIPSCode();
        }

        /**
         *
         * @return
         */
        @Override
        public Level2[] getSubDivisions() {
            return c_divisions != null ? c_divisions.toArray(new Level2[c_divisions.size()]) : new Level2[0];
        }
    }

    /**
     *
     */
    public static class Level2 extends Site<Site<?>> {

        /**
         *
         * @param parent
         * @param type
         */
        public Level2(Site<Level2> parent, Designation type) {
            super(type);
            c_parent = parent;
            c_fipscode = parent.getFIPSCode();
        }

        /**
         *
         * @return
         */
        @Override
        public Site<?>[] getSubDivisions() {
            return c_divisions != null ? c_divisions.toArray(new Site<?>[c_divisions.size()]) : new Site<?>[0];
        }
    }

    /**
     * Constants from FIPS 414, may need to be updated as the fips data changes
     */
    public enum Designation {

        /**
         *
         */
        Country("country"),
        /**
         *
         */
        State("state", "federal state"),
        /**
         *
         */
        District("district", "metropolitan district", "london borough", "commonwealth district",
                "federal district", "capital district", "special district"),
        /**
         *
         */
        County("county", "urban county", "county borough"),
        /**
         *
         */
        Parish,
        /**
         *
         */
        Borough,
        /**
         *
         */
        Dependency,
        /**
         *
         */
        Emirate,
        /**
         *
         */
        Province("province", "constitutional province", "autonomous province"),
        /**
         *
         */
        Rayon,
        /**
         *
         */
        City("city", "municipality", "special municipality", "town", "special city", "chartered city",
                "capital city", "city corporation", "urban commune", "community", "city and county"),
        /**
         *
         */
        Territory("territory", "federal territory", "union territory", "national capital territory",
                "autonomous territorial unit", "territorial unit", "capital territory"),
        /**
         *
         */
        Municipality,
        /**
         *
         */
        Department,
        /**
         *
         */
        Division,
        /**
         *
         */
        Republic("republic", "autonomous republic"),
        /**
         *
         */
        Federation,
        /**
         *
         */
        Region("region", "autonomous region", "special regioin"),
        /**
         *
         */
        Island("island", "islands", "island group", "islands area"),
        /**
         *
         */
        Commune,
        /**
         *
         */
        Prefecture("prefecture", "economic prefecture"),
        /**
         *
         */
        Governorate,
        /**
         *
         */
        Administration,
        /**
         *
         */
        Quarter,
        /**
         *
         */
        Other("special zone", "special region", "capital - special zone", "kray", "oblast",
                "cercle", "zone", "canton", "autonomous okrug", "intendancy", "unitary authority",
                "autonomous oblast", "autonomous community", "statutory community", "ward", "federal dependencies"),
        /**
         *
         */
        Area("area", "Pakistan-administered area", "council area", "administrative area"),
        /**
         *
         */
        Unknown("?");
        private final String[] c_names;

        private Designation() {
            c_names = null;
        }

        private Designation(String... names) {
            c_names = names;
        }

        /**
         *
         * @return
         */
        public String[] getAlternativeNames() {
            return c_names != null ? c_names : new String[]{this.toString()};
        }
    }

    static {
        long startTime = System.nanoTime();
        LOGGER.info("Initilizing site data.");
        //load the site data
        COUNTRIES_FIPS_CHAR2 = new HashMap<String, Level0>();
        SITES_FIPS_CHAR4 = new HashMap<String, Site<?>>();
        SITES_HASC = new HashMap<String, Site<?>>();

        //read fips-414.txt
        read(ClassLoader.getSystemResourceAsStream("usda/weru/resources/site_fips-414.txt"), new LineHandler() {

            @Override
            public void handle(String line) {
                try {
                    String[] parts = line.split("_", -1);
/**
                 * Note:  Assertions are not enabled.  These will be useless items
                 * unless assertions are enabled.  Thus, they will be commented out unless
                 * the user wishes to enable specific assertions (feed the virtual machine 
                 * the -ea argument).
                 */
//                    assert parts.length == 10;
                    String code = parts[0];
                    String desginationText = parts[3];
                    Designation desgination = findDesignation(desginationText);
                    String name = parts[7];

                    code = code.trim();
/**
                 * Note:  Assertions are not enabled.  These will be useless items
                 * unless assertions are enabled.  Thus, they will be commented out unless
                 * the user wishes to enable specific assertions (feed the virtual machine 
                 * the -ea argument).
                 */
//                    assert code.length() == 4;

                    String char2 = code.substring(0, 2);
/**
                 * Note:  Assertions are not enabled.  These will be useless items
                 * unless assertions are enabled.  Thus, they will be commented out unless
                 * the user wishes to enable specific assertions (feed the virtual machine 
                 * the -ea argument).
                 */
//                    assert char2.length() == 2 : "Country code must be two characters.";

                    String divisionString = code.substring(2);
/**
                 * Note:  Assertions are not enabled.  These will be useless items
                 * unless assertions are enabled.  Thus, they will be commented out unless
                 * the user wishes to enable specific assertions (feed the virtual machine 
                 * the -ea argument).
                 */
//                    assert divisionString.length() == 2 : "Division string must be two characters";

                    if (desgination == null) {
                        LOGGER.debug("Missing desgination enum: " + desginationText);
                        desgination = Designation.Unknown;
                    }

                    //this is a country
                    switch (desgination) {
                        case Country:
                            Site.Level0 country = new Site.Level0(desgination, code);
                            country.c_displayName = name;
                            country.c_fips = code.substring(0, 2);
                            country.setFIPSCode(code.substring(0, 2));
                            //top level
                            COUNTRIES_FIPS_CHAR2.put(country.getFIPSChar2(), country);
                            SITES_FIPS_CHAR4.put(country.getFIPSChar4(), country);
                            break;
                        default:
                            Level0 parent = COUNTRIES_FIPS_CHAR2.get(char2);

                            if (parent == null) {
                                LOGGER.warn("No country found. " + char2);
                                return;
                            }

                            Level1 division = new Level1(parent, desgination);
                            division.c_displayName = name;

                            //default unique key is the four character code
                            division.c_primaryKey = code;

                            division.c_fips = divisionString;
                            
                            division.setFIPSCode(parent.getFIPSCode() + "-" + code.substring(0, 4));

                            //add to the parent
                            parent.getSubDivisionList().add(division);

                            SITES_FIPS_CHAR4.put(code, division);
                            break;

                    }

                } catch (Exception e) {
                    LOGGER.debug("Unable to load site: " + (line != null ? line : "<NULL>"), e);
                }

            }
        });

        //iso to fips differences
        final Map<String, String> isoToFips = new HashMap<>();

        read(ClassLoader.getSystemResourceAsStream("usda/weru/resources/site_iso2fips.txt"), new LineHandler() {

            @Override
            public void handle(String line) {
                String[] parts = line.split("_", -1);
/**
                 * Note:  Assertions are not enabled.  These will be useless items
                 * unless assertions are enabled.  Thus, they will be commented out unless
                 * the user wishes to enable specific assertions (feed the virtual machine 
                 * the -ea argument).
                 */
//                assert parts.length == 2;
                String iso = parts[0];
                String fips = parts[1];

                isoToFips.put(iso, fips);

            }
        });

        //read iso-3166.txt
        read(ClassLoader.getSystemResourceAsStream("usda/weru/resources/site_iso-3166.txt"), new LineHandler() {

            @Override
            public void handle(String line) {
                String[] parts = line.split("_", -1);
/**
                 * Note:  Assertions are not enabled.  These will be useless items
                 * unless assertions are enabled.  Thus, they will be commented out unless
                 * the user wishes to enable specific assertions (feed the virtual machine 
                 * the -ea argument).
                 */
//                assert parts.length == 4;

                String char2 = parts[0];
                String char3 = parts[1];
                int number = Integer.valueOf(parts[2]);
                //String name = parts[3];

                //handle differences between fips and iso
                String fips = isoToFips.get(char2);

                Level0 country = COUNTRIES_FIPS_CHAR2.get(fips != null ? fips : char2);
                if (country != null) {
                    country.c_iso3166Char2 = char2;
                    country.c_iso3166Char3 = char3;
                    country.c_iso3166Number = number;

                    //overide with the nicer ISO Names
                    //country.c_displayName = name;
                } else {
                    //oops
                    LOGGER.debug("No country record found: " + line);
                }

            }
        });

        //read us-counties.txt
        read(ClassLoader.getSystemResourceAsStream("usda/weru/resources/site_us-counties.txt"), new LineHandler() {

            @Override
            public void handle(String line) {

                //county line
                String[] parts = line.split("_", -1);
/**
                 * Note:  Assertions are not enabled.  These will be useless items
                 * unless assertions are enabled.  Thus, they will be commented out unless
                 * the user wishes to enable specific assertions (feed the virtual machine 
                 * the -ea argument).
                 */
//               assert parts.length == 5;

                String stateAbbr = parts[0];
                String stateFips = parts[1];

                String countyFips = parts[2];
                String name = parts[3];

                //set the state abbr/key
                //Site state = valueOfFIPS("US" + stateFips);
                Site<Level2> state = Caster.<Site<Level2>>cast(Site.valueOfFIPS("US" + stateFips));
                state.setFIPSCode("US-" + line);

                if (state == null) {
                    LOGGER.debug("No state record found: " + line);
                    return;
                }
                state.c_abbr = stateAbbr;
                state.c_primaryKey = stateAbbr;

                name = name.trim();

                Designation countyEquivalent = Designation.County;
                //hardcoded hack for AK, LA
                if ("LA".equalsIgnoreCase(stateAbbr)) {
                    countyEquivalent = Designation.Parish;
                } else if ("AK".equalsIgnoreCase(stateAbbr)) {
                    countyEquivalent = Designation.Borough;
                }
                Level2 county = new Level2(state, countyEquivalent);
                county.c_displayName = name;
                county.c_primaryKey = countyFips;
                state.getSubDivisionList().add(county);
            }
        });

        //latlongs
        read(ClassLoader.getSystemResourceAsStream("usda/weru/resources/site_latlong.txt"), new LineHandler() {

            @Override
            public void handle(String line) {
                String[] parts = line.split("_", -1);
/**
                 * Note:  Assertions are not enabled.  These will be useless items
                 * unless assertions are enabled.  Thus, they will be commented out unless
                 * the user wishes to enable specific assertions (feed the virtual machine 
                 * the -ea argument).
                 */
//                assert parts.length == 3;

                String code = parts[0];

                Site<?> site = Site.valueOf(code);

                if (site == null) {
                    LOGGER.warn("Unknown site for latlong: " + line);
                    return;
                }

                String latString = parts[1];
                String lonString = parts[2];

                double lat = Double.valueOf(latString);
                double lon = Double.valueOf(lonString);

                LatLong latlong = LatLong.valueOf(lat, lon, NonSI.DEGREE_ANGLE);

                site.c_latlongFallback = latlong;

            }
        });

        //hasc to fips
        read(ClassLoader.getSystemResourceAsStream("usda/weru/resources/site_hasc2fips.txt"), new LineHandler() {

            @Override
            public void handle(String line) {
                String[] parts = line.split("_", -1);
/**
                 * Note:  Assertions are not enabled.  These will be useless items
                 * unless assertions are enabled.  Thus, they will be commented out unless
                 * the user wishes to enable specific assertions (feed the virtual machine 
                 * the -ea argument).
                 */
//                assert parts.length == 2;
                String hasc = parts[0];
                String fips = parts[1];

                Site<?> site = valueOfFIPS(fips);
                if (site != null) {
                    SITES_HASC.put(hasc, site);
                    site.c_hasc = hasc;
                } else {
                    LOGGER.debug("Unable to find site for HASC referenced FIPS code: " + fips);
                }

            }
        });

        Measurable<Duration> elapsedTime = Measure.valueOf(System.nanoTime() - startTime, SI.NANO(SI.SECOND));
        LOGGER.info("Finished initializing site data: "
                + DecimalFormat.getInstance().format(elapsedTime.doubleValue(SI.SECOND)) + " s");

    }

    private static Designation findDesignation(String text) {
        if (text == null) {
            return null;
        }
        text = text.trim();
        for (Designation type : Designation.values()) {
            for (String name : type.getAlternativeNames()) {
                if (name.toLowerCase().equals(text.toLowerCase())) {
                    return type;
                }
            }
        }
        return null;
    }

    /**
     *
     */
    public static class LevelComparator implements Comparator<Site<?>> {

        /**
         *
         * @param a
         * @param b
         * @return
         */
        @Override
        public int compare(Site<?> a, Site<?> b) {
            if (isAncestor(a, b)) {
                return -1;
            } else if (isAncestor(b, a)) {
                return 1;
            } else {
                return 0;
            }
        }

        private boolean isAncestor(Site<?> parent, Site<?> child) {
            while (child != null) {
                if (parent.equals(child)) {
                    return true;
                }
                child = child.getParent();
            }
            return false;
        }
    }
    //useful constants

    /**
     *
     */
    public static final Site.Level0 UNITED_STATES = (Site.Level0) Site.valueOfFIPS("US00");

    /**
     *
     */
    public static final Site.Level0 CHINA = (Site.Level0) Site.valueOfFIPS("CH00");

    private static interface LineHandler {

        public void handle(String line);
    }

    private static void read(InputStream in, LineHandler handler) {
        BufferedReader reader = null;
        try {
            reader = new BufferedReader(new InputStreamReader(in));

            for (String line = reader.readLine(); line != null; line = reader.readLine()) {
                line = line.trim();
                if (line.startsWith("#") || line.length() == 0) {
                    //skip comments or blank lines
                    continue;
                }

                //pass the work off to the line handler
                try {
                    handler.handle(line);
                } catch (Exception e) {
                }

            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

        }
    }
}
