/*
 * 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.apache.tika.parser.journal;

import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import org.apache.tika.exception.TikaException;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.utils.XMLReaderUtils;

public class TEIDOMParser {

    public TEIDOMParser() {
    }

    //returns first child with this name, null otherwise
    private static Node getFirstChild(NodeList childNodes, String name) {
        for (int i = 0; i < childNodes.getLength(); i++) {
            Node n = childNodes.item(i);
            if (n.getNodeName().equals(name)) {
                return n;
            }
        }
        return null;
    }

    private static String getFirstAttribute(Node node, String ns, String name) {
        if (node.hasAttributes()) {
            NamedNodeMap attrs = node.getAttributes();
            for (int i = 0; i < attrs.getLength(); i++) {
                Node attr = attrs.item(i);
                if (attr.getLocalName().equals(name)) {
                    return attr.getNodeValue();
                }
            }
        }
        return null;
    }

    private static List<Node> getChildNodes(NodeList childNodes, String localName) {
        List<Node> ret = new ArrayList<>();
        for (int i = 0; i < childNodes.getLength(); i++) {
            Node child = childNodes.item(i);
            if (child.getLocalName() != null && child.getLocalName().equals(localName)) {
                ret.add(child);
            }
        }
        return ret;
    }

    public Metadata parse(String source, ParseContext parseContext)
            throws TikaException, SAXException, IOException {

        Document root = XMLReaderUtils
                .buildDOM(new StringReader(source), parseContext);

        Metadata metadata = new Metadata();
        createGrobidMetadata(source, root.getDocumentElement(), metadata);
        return metadata;
    }

    private void createGrobidMetadata(String source, Element root, Metadata metadata) {
        if (root != null) {

            Node text = getFirstChild(root.getChildNodes(), "text");
            if (text != null) {
                parseText(text, metadata);
            }
            Node teiHeader = getFirstChild(root.getChildNodes(), "teiHeader");
            Node fileDesc = getFirstChild(teiHeader.getChildNodes(), "fileDesc");
            if (fileDesc != null) {
                parseFileDesc(fileDesc, metadata);

            }
            Node profileDesc = getFirstChild(teiHeader.getChildNodes(), "profileDesc");
            if (profileDesc != null) {
                parseProfileDesc(profileDesc, metadata);
            }

        }

        addStaticMet(source, root, metadata);
    }

    private void addStaticMet(String source, Element obj, Metadata metadata) {
        metadata.add("Class", Metadata.class.getName());
        //no longer available after we got rid of json.org's and its .toJSONObject()
//        metadata.add("TEIJSONSource", obj.toString());
        metadata.add("TEIXMLSource", source);
    }

    private void parseText(Node text, Metadata metadata) {
        String lang = getFirstAttribute(text, "xml", "lang");
        if (lang != null) {
            metadata.add("Language", lang);
        }
    }

    private void parseFileDesc(Node fileDesc, Metadata metadata) {
        Node titleStmt = getFirstChild(fileDesc.getChildNodes(), "titleStmt");

        if (titleStmt != null) {
            parseTitleStmt(titleStmt, metadata);
        }

        Node sourceDesc = getFirstChild(fileDesc.getChildNodes(), "sourceDesc");
        if (sourceDesc != null) {
            parseSourceDesc(sourceDesc, metadata);
        }
    }

    private void parseTitleStmt(Node titleStmt, Metadata metadata) {
        Node title = getFirstChild(titleStmt.getChildNodes(), "title");
        if (title != null) {
            String titleText = title.getTextContent();
            if (titleText != null) {
                metadata.add("Title", titleText);
            }
        }
    }

    private void parseSourceDesc(Node sourceDesc, Metadata metadata) {
        Node biblStruct = getFirstChild(sourceDesc.getChildNodes(), "biblStruct");
        if (biblStruct != null) {
            parseBiblStruct(biblStruct, metadata);
        }
    }

    private void parseBiblStruct(Node biblStruct, Metadata metadata) {

        Node analytic = getFirstChild(biblStruct.getChildNodes(), "analytic");
        if (analytic != null) {
            List<Node> authorNodes = getChildNodes(analytic.getChildNodes(), "author");
            List<Author> authorList = new ArrayList<>();
            for (Node authorNode : authorNodes) {
                parseAuthor(authorNode, 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<>();
        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<>();
        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<>();
        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<>();
        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(Node authorNode, List<Author> authorList) {
        Author author = new Author();
        Node persName = getFirstChild(authorNode.getChildNodes(), "persName");
        if (persName != null) {
            List<Node> forenames = getChildNodes(persName.getChildNodes(), "forename");
            for (Node forenameNode : forenames) {
                parseNamePart(forenameNode, author);
            }
            Node surnameNode = getFirstChild(persName.getChildNodes(), "surname");
            if (surnameNode != null) {
                String surnameContent = surnameNode.getTextContent();
                if (surnameContent != null) {
                    author.setSurName(surnameContent);
                }
            }
        }
        List<Node> affiliationNodes = getChildNodes(authorNode.getChildNodes(), "affiliation");
        for (Node affiliationNode : affiliationNodes) {
            parseOneAffiliation(affiliationNode, author);
        }


        authorList.add(author);
    }

    private void parseNamePart(Node namePart, Author author) {
        String type = getFirstAttribute(namePart, null, "type");
        String content = namePart.getTextContent();
        if (type != null && content != null) {

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

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

    private void parseOneAffiliation(Node affiliationNode, Author author) {

        Affiliation affiliation = new Affiliation();
        Node address = getFirstChild(affiliationNode.getChildNodes(), "address");
        if (address != null) {
            parseAddress(address, affiliation);
        }

        List<Node> orgNameNodes = getChildNodes(affiliationNode.getChildNodes(), "orgName");
        OrgName orgName = new OrgName();
        for (Node orgNameNode : orgNameNodes) {
            parseOrgName(orgNameNode, orgName);
        }
        affiliation.setOrgName(orgName);

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

    private void parseAddress(Node addressNode, Affiliation affiliation) {
        Address address = new Address();
        Node region = getFirstChild(addressNode.getChildNodes(), "region");
        if (region != null && region.getTextContent() != null) {
            address.setRegion(region.getTextContent());
        }
        Node postCode = getFirstChild(addressNode.getChildNodes(), "postCode");
        if (postCode != null && postCode.getTextContent() != null) {
            address.setPostCode(postCode.getTextContent());
        }
        Node settlementNode = getFirstChild(addressNode.getChildNodes(), "settlement");
        if (settlementNode != null && settlementNode.getTextContent() != null) {
            address.setSettlment(settlementNode.getTextContent());
        }

        Node countryNode = getFirstChild(addressNode.getChildNodes(), "country");
        if (countryNode != null) {
            Country country = new Country();
            String key = getFirstAttribute(countryNode, null, "key");
            if (key != null) {
                country.setKey(key);
            }
            String content = countryNode.getTextContent();
            if (content != null) {
                country.setContent(content);
            }
            address.setCountry(country);
        }

        affiliation.setAddress(address);
    }

    private void parseOrgName(Node orgNode, OrgName orgName) {
        OrgTypeName typeName = new OrgTypeName();
        String orgContent = orgNode.getTextContent();
        if (orgContent != null) {
            typeName.setName(orgContent);
        }
        String orgType = getFirstAttribute(orgNode, null, "type");
        if (orgType != null) {
            typeName.setType(orgType);
        }

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

    private void parseProfileDesc(Node profileDesc, Metadata metadata) {
        Node abstractNode = getFirstChild(profileDesc.getChildNodes(), "abstract");
        if (abstractNode != null) {
            Node pNode = getFirstChild(abstractNode.getChildNodes(), "p");
            if (pNode != null) {
                metadata.add("Abstract", pNode.getTextContent());
            }
        }

        Node textClassNode = getFirstChild(profileDesc.getChildNodes(), "textClass");
        if (textClassNode != null) {
            Node keywordsNode = getFirstChild(textClassNode.getChildNodes(), "keywords");
            if (keywordsNode != null) {
                List<Node> terms = getChildNodes(keywordsNode.getChildNodes(), "term");
                if (terms.size() == 0) {
                    // test AJ15.pdf
                    metadata.add("Keyword", keywordsNode.getTextContent());
                } else {
                    for (Node term : terms) {
                        metadata.add("Keyword", term.getTextContent());
                    }
                }

            }
        }

    }

    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<>();
        }

        /**
         * @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 + "]";
        }

    }

    static 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());

        }

        @Override
        public int hashCode() {
            return Objects.hash(orgName, address);
        }

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

    }

    static class OrgName {
        private List<OrgTypeName> typeNames;

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

        /**
         * @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();
        }

        @Override
        public int hashCode() {
            return Objects.hash(typeNames);
        }

        /*
         * (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;
                }
            }

        }

    }

    static 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());
        }

        @Override
        public int hashCode() {
            return Objects.hash(name, type);
        }
    }

    private static 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());
        }

        @Override
        public int hashCode() {
            return Objects.hash(region, postCode, settlment, country);
        }

        /*
         * (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 static 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());
                }
            }
        }

        @Override
        public int hashCode() {
            return Objects.hash(key, content);
        }
    }

}
