/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements. See the NOTICE file distributed with this
 * work for additional information regarding copyright ownership. The ASF
 * licenses this file to You under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package org.vlir.darkaiv.md_extractor.util;

import java.util.ArrayList;
import java.util.List;

import org.apache.tika.metadata.Metadata;
import org.json.JSONArray;
import org.json.JSONObject;
import org.json.JSONException;
import org.json.XML;

public class TikaTEIParser {

    public TikaTEIParser() {
    }

    public Metadata parse(String source) {
        JSONObject obj = XML.toJSONObject(source);
        Metadata metadata = new Metadata();
        createGrobidMetadata(source, obj, metadata);
        return metadata;
    }

    private void createGrobidMetadata(String source, JSONObject obj,
            Metadata metadata) {
        if (obj != null) {
            try {
                JSONObject teiHeader = obj.getJSONObject("TEI")
                        .getJSONObject("teiHeader");
                if (teiHeader.has("text")) {
                    parseText(teiHeader.getJSONObject("text"), metadata);
                }

                if (teiHeader.has("fileDesc")) {
                    parseFileDesc(teiHeader.getJSONObject("fileDesc"), metadata);

                }
                if (teiHeader.has("profileDesc")) {
                    parseProfileDesc(teiHeader.getJSONObject("profileDesc"), metadata);
                }
            } catch (JSONException e) {
                System.out.println("No TEI Object found.");
            }
        }

        addStaticMet(source, obj, metadata);
    }

    private void addStaticMet(String source, JSONObject obj, Metadata metadata) {
        metadata.add("Class", Metadata.class.getName());
        metadata.add("TEIJSONSource", obj.toString());
        metadata.add("TEIXMLSource", source);
    }

    private void parseText(JSONObject text, Metadata metadata) {
        if (text.has("xml:lang")) {
            metadata.add("Language", text.getString("xml:lang"));
        }
    }

    private void parseFileDesc(JSONObject fileDesc, Metadata metadata) {
        if (fileDesc.has("titleStmt")) {
            parseTitleStmt(fileDesc.getJSONObject("titleStmt"), metadata);
        }

        if (fileDesc.has("sourceDesc")) {
            parseSourceDesc(fileDesc.getJSONObject("sourceDesc"), metadata);
        }
    }

    private void parseTitleStmt(JSONObject titleStmt, Metadata metadata) {
        if (titleStmt.has("title")) {
            JSONObject title = titleStmt.getJSONObject("title");
            if (title.has("content")) {
                metadata.add("Title", title.getString("content"));
            }
        }
    }

    private void parseSourceDesc(JSONObject sourceDesc, Metadata metadata) {
        if (sourceDesc.has("biblStruct")) {
            parseBiblStruct(sourceDesc.getJSONObject("biblStruct"), metadata);
        }
    }

    private void parseBiblStruct(JSONObject biblStruct, Metadata metadata) {
        if (biblStruct.has("analytic")
                && biblStruct.get("analytic") instanceof JSONObject) {
            JSONObject analytic = biblStruct.getJSONObject("analytic");
            if (analytic.has("author")) {
                Object authorObj = analytic.get("author");

                List<Author> authorList = new ArrayList<Author>();
                if (authorObj instanceof JSONObject) {
                    parseAuthor((JSONObject) authorObj, authorList);
                } else if (authorObj instanceof JSONArray) {
                    JSONArray authors = (JSONArray) authorObj;
                    if (authors.length() > 0) {
                        for (int i = 0; i < authors.length(); i++) {
                            JSONObject author = authors.getJSONObject(i);
                            parseAuthor(author, authorList);
                        }
                    }

                    metadata.add("Address", getMetadataAddresses(authorList));
                    metadata.add("Affiliation", getMetadataAffiliations(authorList));
                    metadata.add("Authors", getMetadataAuthors(authorList));
                    metadata.add("FullAffiliations",
                            getMetadataFullAffiliations(authorList));
                }

            }
        } else {
            metadata.add("Error", "Unable to parse: no analytic section in JSON");
        }

    }

    private String getMetadataFullAffiliations(List<Author> authorList) {
        List<Affiliation> unique = new ArrayList<Affiliation>();
        StringBuilder metAffils = new StringBuilder();

        for (Author a : authorList) {
            for (Affiliation af : a.getAffiliations()) {
                if (!unique.contains(af)) {
                    unique.add(af);
                }
            }
        }
        metAffils.append("[");
        for (Affiliation af : unique) {
            metAffils.append(af.toString());
            metAffils.append(",");
        }
        metAffils.append(metAffils.deleteCharAt(metAffils.length() - 1));
        metAffils.append("]");
        return metAffils.toString();
    }

    private String getMetadataAuthors(List<Author> authorList) {
        // generates Chris A. Mattmann 1, 2 Daniel J. Crichton 1 Nenad Medvidovic 2
        // Steve Hughes 1
        List<Affiliation> unique = new ArrayList<Affiliation>();
        StringBuilder metAuthors = new StringBuilder();

        for (Author a : authorList) {
            for (Affiliation af : a.getAffiliations()) {
                if (!unique.contains(af)) {
                    unique.add(af);
                }
            }
        }

        for (Author a : authorList) {
            metAuthors.append(printOrBlank(a.getFirstName()));
            metAuthors.append(printOrBlank(a.getMiddleName()));
            metAuthors.append(printOrBlank(a.getSurName()));

            StringBuilder affilBuilder = new StringBuilder();
            for (int idx = 0; idx < unique.size(); idx++) {
                Affiliation af = unique.get(idx);
                if (a.getAffiliations().contains(af)) {
                    affilBuilder.append((idx + 1));
                    affilBuilder.append(",");
                }
            }

            if (affilBuilder.length() > 0) {
                affilBuilder.deleteCharAt(affilBuilder.length() - 1);
            }

            metAuthors.append(affilBuilder.toString());
            metAuthors.append(" ");
        }

        return metAuthors.toString();
    }

    private String getMetadataAffiliations(List<Author> authorList) {
        // generates 1 Jet Propulsion Laboratory California Institute of Technology
        // ; 2 Computer Science Department University of Southern California
        List<Affiliation> unique = new ArrayList<Affiliation>();
        StringBuilder metAffil = new StringBuilder();

        for (Author a : authorList) {
            for (Affiliation af : a.getAffiliations()) {
                if (!unique.contains(af)) {
                    unique.add(af);
                }
            }
        }

        int count = 1;
        for (Affiliation a : unique) {
            metAffil.append(count);
            metAffil.append(" ");
            metAffil.append(a.getOrgName().toString());
            metAffil.deleteCharAt(metAffil.length() - 1);
            metAffil.append("; ");
            count++;
        }

        if (count > 1) {
            metAffil.deleteCharAt(metAffil.length() - 1);
            metAffil.deleteCharAt(metAffil.length() - 1);
        }

        return metAffil.toString();
    }

    private String getMetadataAddresses(List<Author> authorList) {
        // generates: "Pasadena, CA 91109, USA Los Angeles, CA 90089, USA",
        List<Address> unique = new ArrayList<Address>();
        StringBuilder metAddress = new StringBuilder();

        for (Author a : authorList) {
            for (Affiliation af : a.getAffiliations()) {
                if (!unique.contains(af.getAddress())) {
                    unique.add(af.getAddress());
                }
            }
        }

        for (Address ad : unique) {
            metAddress.append(ad.toString());
            metAddress.append(" ");
        }

        return metAddress.toString();
    }

    private void parseAuthor(JSONObject authorObj, List<Author> authorList) {
        Author author = new Author();

        if (authorObj.has("persName")) {
            JSONObject persName = authorObj.getJSONObject("persName");

            if (persName.has("forename")) {

                Object foreNameObj = persName.get("forename");

                if (foreNameObj instanceof JSONObject) {
                    parseNamePart((JSONObject) foreNameObj, author);
                } else if (foreNameObj instanceof JSONArray) {
                    JSONArray foreName = persName.getJSONArray("forename");

                    if (foreName.length() > 0) {
                        for (int i = 0; i < foreName.length(); i++) {
                            JSONObject namePart = foreName.getJSONObject(i);
                            parseNamePart(namePart, author);
                        }
                    }
                }
            }

            if (persName.has("surname")) {
                author.setSurName(persName.getString("surname"));
            }

            if (authorObj.has("affiliation")) {
                parseAffiliation(authorObj.get("affiliation"), author);
            }

        }

        authorList.add(author);
    }

    private void parseNamePart(JSONObject namePart, Author author) {
        if (namePart.has("type") && namePart.has("content")) {
            String type = namePart.getString("type");
            String content = namePart.getString("content");

            if (type.equals("first")) {
                author.setFirstName(content);
            }

            if (type.equals("middle")) {
                author.setMiddleName(content);
            }
        }
    }

    private void parseAffiliation(Object affiliationJSON, Author author) {
        if (affiliationJSON instanceof JSONObject) {
            parseOneAffiliation((JSONObject) affiliationJSON, author);
        } else if (affiliationJSON instanceof JSONArray) {
            JSONArray affiliationArray = (JSONArray) affiliationJSON;
            if (affiliationArray != null && affiliationArray.length() > 0) {
                for (int i = 0; i < affiliationArray.length(); i++) {
                    JSONObject affiliationObj = affiliationArray.getJSONObject(i);
                    parseOneAffiliation(affiliationObj, author);
                }
            }
        }
    }

    private void parseOneAffiliation(JSONObject affiliationObj, Author author) {

        Affiliation affiliation = new Affiliation();
        if (affiliationObj.has("address")) {
            parseAddress(affiliationObj.getJSONObject("address"), affiliation);
        }

        if (affiliationObj.has("orgName")) {
            OrgName orgName = new OrgName();
            Object orgObject = affiliationObj.get("orgName");
            if (orgObject instanceof JSONObject) {
                parseOrgName((JSONObject) orgObject, orgName);
            } else if (orgObject instanceof JSONArray) {
                JSONArray orgNames = (JSONArray) orgObject;
                if (orgNames != null && orgNames.length() > 0) {
                    for (int i = 0; i < orgNames.length(); i++) {
                        parseOrgName(orgNames.getJSONObject(i), orgName);
                    }
                }

                affiliation.setOrgName(orgName);
            }

        }

        author.getAffiliations().add(affiliation);
    }

    private void parseAddress(JSONObject addressObj, Affiliation affiliation) {
        Address address = new Address();

        if (addressObj.has("region")) {
            address.setRegion(addressObj.getString("region"));
        }

        if (addressObj.has("postCode")) {
            address.setPostCode(JSONObject.valueToString(addressObj.get("postCode")));
        }

        if (addressObj.has("settlement")) {
            address.setSettlment(addressObj.getString("settlement"));
        }

        if (addressObj.has("country")) {
            Country country = new Country();
            Object countryObj = addressObj.get("country");

            if (countryObj instanceof JSONObject) {
                JSONObject countryJson = addressObj.getJSONObject("country");

                if (countryJson.has("content")) {
                    country.setContent(countryJson.getString("content"));
                }

                if (countryJson.has("key")) {
                    country.setKey(countryJson.getString("key"));
                }
            } else if (countryObj instanceof String) {
                country.setContent((String) countryObj);
            }
            address.setCountry(country);
        }

        affiliation.setAddress(address);
    }

    private void parseOrgName(JSONObject orgObj, OrgName orgName) {
        OrgTypeName typeName = new OrgTypeName();
        if (orgObj.has("content")) {
            typeName.setName(orgObj.getString("content"));
        }

        if (orgObj.has("type")) {
            typeName.setType(orgObj.getString("type"));
        }

        orgName.getTypeNames().add(typeName);
    }

    private void parseProfileDesc(JSONObject profileDesc, Metadata metadata) {
        if (profileDesc.has("abstract")) {
            if (profileDesc.has("p")) {
                metadata.add("Abstract", profileDesc.getString("p"));
            }
        }

        if (profileDesc.has("textClass")) {
            JSONObject textClass = profileDesc.getJSONObject("textClass");

            if (textClass.has("keywords")) {
                Object keywordsObj = textClass.get("keywords");
                // test AJ15.pdf
                if (keywordsObj instanceof String) {
                    metadata.add("Keyword", (String) keywordsObj);
                } else if (keywordsObj instanceof JSONObject) {
                    JSONObject keywords = textClass.getJSONObject("keywords");
                    if (keywords.has("term")) {
                        JSONArray termArr = keywords.getJSONArray("term");
                        for (int i = 0; i < termArr.length(); i++) {
                            metadata.add("Keyword", JSONObject.valueToString(termArr.get(i)));
                        }
                    }
                }

            }
        }

    }

    private String printOrBlank(String val) {
        if (val != null && !val.equals("")) {
            return val + " ";
        } else {
            return " ";
        }
    }

    class Author {

        private String surName;

        private String middleName;

        private String firstName;

        private List<Affiliation> affiliations;

        public Author() {
            this.surName = null;
            this.middleName = null;
            this.firstName = null;
            this.affiliations = new ArrayList<Affiliation>();
        }

        /**
         * @return the surName
         */
        public String getSurName() {
            return surName;
        }

        /**
         * @param surName the surName to set
         */
        public void setSurName(String surName) {
            this.surName = surName;
        }

        /**
         * @return the middleName
         */
        public String getMiddleName() {
            return middleName;
        }

        /**
         * @param middleName the middleName to set
         */
        public void setMiddleName(String middleName) {
            this.middleName = middleName;
        }

        /**
         * @return the firstName
         */
        public String getFirstName() {
            return firstName;
        }

        /**
         * @param firstName the firstName to set
         */
        public void setFirstName(String firstName) {
            this.firstName = firstName;
        }

        /**
         * @return the affiliations
         */
        public List<Affiliation> getAffiliations() {
            return affiliations;
        }

        /**
         * @param affiliations the affiliations to set
         */
        public void setAffiliations(List<Affiliation> affiliations) {
            this.affiliations = affiliations;
        }

        /*
         * (non-Javadoc)
         * 
         * @see java.lang.Object#toString()
         */
        @Override
        public String toString() {
            return "Author [surName=" + surName + ", middleName=" + middleName != null ? middleName
                    : "" + ", firstName=" + firstName + ", affiliations=" + affiliations
                    + "]";
        }

    }

    class Affiliation {

        private OrgName orgName;

        private Address address;

        public Affiliation() {
            this.orgName = new OrgName();
            this.address = new Address();
        }

        /**
         * @return the orgName
         */
        public OrgName getOrgName() {
            return orgName;
        }

        /**
         * @param orgName the orgName to set
         */
        public void setOrgName(OrgName orgName) {
            this.orgName = orgName;
        }

        /**
         * @return the address
         */
        public Address getAddress() {
            return address;
        }

        /**
         * @param address the address to set
         */
        public void setAddress(Address address) {
            this.address = address;
        }

        /*
         * (non-Javadoc)
         * 
         * @see java.lang.Object#equals(java.lang.Object)
         */
        @Override
        public boolean equals(Object obj) {
            Affiliation otherA = (Affiliation) obj;
            return this.getAddress().equals(otherA.getAddress())
                    && this.getOrgName().equals(otherA.getOrgName());

        }

        /*
         * (non-Javadoc)
         * 
         * @see java.lang.Object#toString()
         */
        @Override
        public String toString() {
            return "Affiliation {orgName=" + orgName + ", address=" + address + "}";
        }

    }

    class OrgName {

        private List<OrgTypeName> typeNames;

        public OrgName() {
            this.typeNames = new ArrayList<OrgTypeName>();
        }

        /**
         * @return the typeNames
         */
        public List<OrgTypeName> getTypeNames() {
            return typeNames;
        }

        /**
         * @param typeNames the typeNames to set
         */
        public void setTypeNames(List<OrgTypeName> typeNames) {
            this.typeNames = typeNames;
        }

        /*
         * (non-Javadoc)
         * 
         * @see java.lang.Object#toString()
         */
        @Override
        public String toString() {
            StringBuilder builder = new StringBuilder();
            for (OrgTypeName on : this.typeNames) {
                builder.append(on.getName());
                builder.append(" ");
            }
            return builder.toString();
        }

        /*
         * (non-Javadoc)
         * 
         * @see java.lang.Object#equals(java.lang.Object)
         */
        @Override
        public boolean equals(Object obj) {
            OrgName otherA = (OrgName) obj;

            if (otherA.getTypeNames() != null) {
                if (this.typeNames == null) {
                    return false;
                } else {
                    return this.typeNames.size() == otherA.getTypeNames().size();
                }
            } else {
                if (this.typeNames == null) {
                    return true;
                } else {
                    return false;
                }
            }

        }

    }

    class OrgTypeName {

        private String name;
        private String type;

        public OrgTypeName() {
            this.name = null;
            this.type = null;
        }

        /**
         * @return the name
         */
        public String getName() {
            return name;
        }

        /**
         * @param name the name to set
         */
        public void setName(String name) {
            this.name = name;
        }

        /**
         * @return the type
         */
        public String getType() {
            return type;
        }

        /**
         * @param type the type to set
         */
        public void setType(String type) {
            this.type = type;
        }

        /*
         * (non-Javadoc)
         * 
         * @see java.lang.Object#equals(java.lang.Object)
         */
        @Override
        public boolean equals(Object obj) {
            OrgTypeName otherOrgName = (OrgTypeName) obj;
            return this.type.equals(otherOrgName.getType())
                    && this.name.equals(otherOrgName.getName());
        }

    }

    private class Address {

        private String region;
        private String postCode;
        private String settlment;
        private Country country;

        public Address() {
            this.region = null;
            this.postCode = null;
            this.settlment = null;
            this.country = new Country();
        }

        /**
         * @return the region
         */
        public String getRegion() {
            return region;
        }

        /**
         * @param region the region to set
         */
        public void setRegion(String region) {
            this.region = region;
        }

        /**
         * @return the postCode
         */
        public String getPostCode() {
            return postCode;
        }

        /**
         * @param postCode the postCode to set
         */
        public void setPostCode(String postCode) {
            this.postCode = postCode;
        }

        /**
         * @return the settlment
         */
        public String getSettlment() {
            return settlment;
        }

        /**
         * @param settlment the settlment to set
         */
        public void setSettlment(String settlment) {
            this.settlment = settlment;
        }

        /**
         * @return the country
         */
        public Country getCountry() {
            return country;
        }

        /**
         * @param country the country to set
         */
        public void setCountry(Country country) {
            this.country = country;
        }

        /*
         * (non-Javadoc)
         * 
         * @see java.lang.Object#equals(java.lang.Object)
         */
        @Override
        public boolean equals(Object obj) {
            Address otherA = (Address) obj;
            if (this.settlment == null) {
                return otherA.getSettlment() == null;
            } else if (this.country == null) {
                return otherA.getCountry() == null;
            } else if (this.postCode == null) {
                return otherA.getPostCode() == null;
            } else if (this.region == null) {
                return otherA.getRegion() == null;
            }

            return this.settlment.equals(otherA.getSettlment())
                    && this.country.equals(otherA.getCountry())
                    && this.postCode.equals(otherA.getPostCode())
                    && this.region.equals(otherA.getRegion());
        }

        /*
         * (non-Javadoc)
         * 
         * @see java.lang.Object#toString()
         */
        @Override
        public String toString() {
            StringBuilder builder = new StringBuilder();
            builder.append(settlment);
            builder.append(", ");
            builder.append(region);
            builder.append(" ");
            builder.append(postCode);
            builder.append(" ");
            builder.append(country.getContent());
            return builder.toString();
        }
    }

    private class Country {

        private String key;
        private String content;

        public Country() {
            this.key = null;
            this.content = null;
        }

        /**
         * @return the key
         */
        public String getKey() {
            return key;
        }

        /**
         * @param key the key to set
         */
        public void setKey(String key) {
            this.key = key;
        }

        /**
         * @return the content
         */
        public String getContent() {
            return content;
        }

        /**
         * @param content the content to set
         */
        public void setContent(String content) {
            this.content = content;
        }

        /*
         * (non-Javadoc)
         * 
         * @see java.lang.Object#equals(java.lang.Object)
         */
        @Override
        public boolean equals(Object obj) {
            Country otherC = (Country) obj;

            if (this.key == null) {
                if (otherC.getKey() != null) {
                    return false;
                } else {
                    if (this.content == null) {
                        if (otherC.getContent() != null) {
                            return false;
                        } else {
                            return true;
                        }
                    } else {
                        return content.equals(otherC.getContent());
                    }
                }
            } else {
                if (this.content == null) {
                    if (otherC.getContent() != null) {
                        return false;
                    } else {
                        return this.key.equals(otherC.getKey());
                    }
                } else {
                    return this.key.equals(otherC.getKey())
                            && this.content.equals(otherC.getContent());
                }
            }
        }

    }
}
