/* UtilLib_Html - Copyright (c) 2024 Vincent Calame - Exemole
 * Logiciel libre donné sous triple licence :
 * 1) selon les termes de la CeCILL V2
 * 2) selon les termes de l’EUPL V.1.1
 * 3) selon les termes de la GNU GPLv3
 * Voir le fichier licences.txt
 */


package net.mapeadores.util.html.jsoup;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import net.mapeadores.util.exceptions.ResponseCodeException;
import net.mapeadores.util.exceptions.ResponseContentTypeException;
import net.mapeadores.util.html.MetaReport;
import net.mapeadores.util.json.JSONArray;
import net.mapeadores.util.json.JSONObject;
import net.mapeadores.util.text.MultiStringable;
import net.mapeadores.util.text.StringUtils;
import org.jsoup.HttpStatusException;
import org.jsoup.Jsoup;
import org.jsoup.UnsupportedMimeTypeException;
import org.jsoup.nodes.DataNode;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;


/**
 *
 * @author Vincent Calame
 */
public class MetaReportEngine {

    private final Map<String, ProtocolValuesBuilder> builderMap = new HashMap<String, ProtocolValuesBuilder>();

    private MetaReportEngine() {

    }

    private MetaReport scan(Document doc) {
        addHtml("title", doc.title());
        addHtml("lang", doc.firstElementChild().attr("lang"));
        Element head = doc.head();
        for (Element meta : head.select("meta[name]")) {
            String name = meta.attr("name");
            String content = meta.attr("content");
            if (content.isEmpty()) {
                continue;
            }
            if (name.startsWith("DC.")) {
                addDublinCore(name.substring(3), content);
            } else {
                addHtml(name, content);
            }
        }
        for (Element meta : head.select("meta[property]")) {
            String name = meta.attr("property");
            String content = meta.attr("content");
            if (content.isEmpty()) {
                continue;
            }
            int idx = name.indexOf(':');
            if (idx > 1) {
                String namespace = name.substring(0, idx);
                if (isOpenGraphNameSpace(namespace)) {
                    addOpenGraph(name, content);
                }
            }
        }
        for (Element script : head.select("script[type='application/ld+json']")) {
            StringBuilder buf = new StringBuilder();
            for (DataNode datanode : script.dataNodes()) {
                buf.append(datanode.getWholeData());
            }
            try {
                JSONObject ldJson = new JSONObject(buf.toString());
                addJsonLd(ldJson);
            } catch (IOException ioe) {
            }
        }
        SortedMap<String, MetaReport.ProtocolValues> protocolMap = new TreeMap<String, MetaReport.ProtocolValues>();
        for (ProtocolValuesBuilder builder : builderMap.values()) {
            protocolMap.put(builder.getName(), builder.toProtocolValues());
        }
        return new InternalMetaReport(protocolMap);
    }

    private void addHtml(String name, String value) {
        ProtocolValuesBuilder builder = getProtocoleValuesBuilder(MetaReport.HTML);
        if (name.equals("keywords")) {
            String[] tokens = StringUtils.getTokens(value, ',', StringUtils.EMPTY_EXCLUDE);
            for (String token : tokens) {
                builder.add(name, token);
            }
        } else {
            builder.add(name, value);
        }
    }

    private void addDublinCore(String name, String value) {
        ProtocolValuesBuilder builder = getProtocoleValuesBuilder(MetaReport.DUBLIN_CORE);
        builder.add(name, value);
    }

    private void addOpenGraph(String name, String value) {
        ProtocolValuesBuilder builder = getProtocoleValuesBuilder(MetaReport.OPENGRAPH);
        builder.add(name, value);
    }

    private void addJsonLd(JSONObject ldJson) {
        ProtocolValuesBuilder builder = getProtocoleValuesBuilder(MetaReport.SCHEMA);
        for (Iterator it = ldJson.keys(); it.hasNext();) {
            String key = (String) it.next();
            Object value = ldJson.get(key);
            if (value instanceof String) {
                builder.add(key, (String) value);
            } else if (value instanceof JSONArray) {
                JSONArray array = (JSONArray) value;
                int length = array.length();
                for (int i = 0; i < length; i++) {
                    Object item = array.get(i);
                    if (item instanceof String) {
                        builder.add(key, (String) item);
                    } else if (item instanceof JSONObject) {
                        JSONObject itemObj = (JSONObject) item;
                        if (itemObj.has("name")) {
                            builder.add(key, itemObj.getString("name"));
                        } else {
                            builder.add(key, itemObj.toString());
                        }
                    } else {
                        builder.add(key, item.toString());
                    }
                }
            } else {
                builder.add(key, value.toString());
            }
        }
    }

    private ProtocolValuesBuilder getProtocoleValuesBuilder(String name) {
        ProtocolValuesBuilder builder = builderMap.get(name);
        if (builder == null) {
            builder = new ProtocolValuesBuilder(name);
            builderMap.put(name, builder);
        }
        return builder;
    }

    public static MetaReport run(String url) throws IOException, ResponseCodeException, ResponseContentTypeException {
        try {
            Document doc = Jsoup.connect(url)
                    .userAgent("Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0")
                    .get();
            return run(doc);
        } catch (HttpStatusException hse) {
            throw new ResponseCodeException(hse.getStatusCode());
        } catch (UnsupportedMimeTypeException umte) {
            throw new ResponseContentTypeException(umte.getMimeType());
        }
    }

    public static MetaReport run(Document doc) {
        MetaReportEngine engine = new MetaReportEngine();
        return engine.scan(doc);
    }

    private static boolean isOpenGraphNameSpace(String namespace) {
        switch (namespace) {
            case "og":
            case "article":
            case "book":
            case "video":
            case "music":
                return true;
            default:
                return false;
        }
    }


    private static class ProtocolValuesBuilder {

        private final String name;
        Map<String, List<String>> listMap = new LinkedHashMap<String, List<String>>();

        private ProtocolValuesBuilder(String name) {
            this.name = name;
        }

        private String getName() {
            return name;
        }

        private void add(String name, String value) {
            if (!value.isEmpty()) {
                List<String> stringList = listMap.get(name);
                if (stringList == null) {
                    stringList = new ArrayList<String>();
                    listMap.put(name, stringList);
                }
                stringList.add(value);
            }
        }

        private MetaReport.ProtocolValues toProtocolValues() {
            Map<String, MultiStringable> finalMap = new LinkedHashMap<String, MultiStringable>();
            for (Map.Entry<String, List<String>> entry : listMap.entrySet()) {
                finalMap.put(entry.getKey(), StringUtils.toMultiStringable(entry.getValue()));
            }
            return new InternalProtocolValues(name, finalMap);
        }

    }


    private static class InternalMetaReport implements MetaReport {

        private final Map<String, MetaReport.ProtocolValues> protocolMap;

        private InternalMetaReport(Map<String, MetaReport.ProtocolValues> protocolMap) {
            this.protocolMap = protocolMap;
        }

        @Override
        public Set<String> getAvalaibleProtocolSet() {
            return Collections.unmodifiableSet(protocolMap.keySet());
        }

        @Override
        public ProtocolValues getProtocolValues(String protocolName) {
            return protocolMap.get(protocolName);
        }

    }


    private static class InternalProtocolValues implements MetaReport.ProtocolValues {

        private final String protocolName;
        private final Map<String, MultiStringable> valueMap;

        private InternalProtocolValues(String protocolName, Map<String, MultiStringable> valueMap) {
            this.protocolName = protocolName;
            this.valueMap = valueMap;
        }

        @Override
        public String getProtocolName() {
            return protocolName;
        }

        @Override
        public Set<String> getParamNameSet() {
            return Collections.unmodifiableSet(valueMap.keySet());
        }

        @Override
        public MultiStringable getValues(String name) {
            return valueMap.get(name);
        }

    }

}
