/**
 * This file is part of the Meeds project (https://meeds.io/).
 *
 * Copyright (C) 2020 - 2025 Meeds Association contact@meeds.io
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */
package org.exoplatform.web.application.javascript;

import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;

import jakarta.servlet.ServletContext;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.exoplatform.commons.utils.I18N;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;
import org.gatein.common.xml.XMLTools;
import org.gatein.portal.controller.resource.ResourceId;
import org.gatein.portal.controller.resource.ResourceScope;
import org.gatein.portal.controller.resource.script.FetchMode;
import org.gatein.portal.controller.resource.script.Module.Local.Content;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

public class JavascriptConfigParser {

  private static final Log       LOG                              = ExoLogger.getExoLogger(JavascriptConfigParser.class);

  public static final String     JAVA_SCRIPT_TAG                  = "javascript";

  public static final String     JAVA_SCRIPT_PARAM                = "param";

  public static final String     JAVA_SCRIPT_MODULE               = "js-module";

  public static final String     JAVA_SCRIPT_PATH                 = "js-path";

  public static final String     JAVA_SCRIPT_PRIORITY             = "js-priority";

  public static final String     JAVA_SCRIPT_PORTAL_NAME          = "portal-name";

  public static final String     LEGACY_JAVA_SCRIPT               = "merged";

  /** . */
  public static final String     SCRIPT_TAG                       = "script";

  public static final String     SCRIPTS_TAG                      = "scripts";

  /** . */
  public static final String     PORTLET_TAG                      = "portlet";

  /** . */
  public static final String     PORTAL_TAG                       = "portal";

  /** . */
  public static final String     RESOURCE_TAG                     = "resource";

  /** . */
  public static final String     SCOPE_TAG                        = "scope";

  /** . */
  public static final String     MODULE_TAG                       = "module";

  /** . */
  public static final String     PATH_TAG                         = "path";

  /** . */
  public static final String     DEPENDS_TAG                      = "depends";

  /** . */
  public static final String     URL_TAG                          = "url";

  /** . */
  public static final String     AS_TAG                           = "as";

  /** . */
  public static final String     ADAPTER_TAG                      = "adapter";

  /** . */
  public static final String     INCLUDE_TAG                      = "include";

  /** . */
  public static final String     GROUP_TAG                        = "load-group";

  /** . */
  private final String           contextPath;

  private DocumentBuilderFactory documentBuilderFactory;

  private static final String    SCRIPT_RESOURCE_DESCRIPTORS_ATTR = "gatein.script.resource.descriptors";

  public JavascriptConfigParser(String contextPath) {
    this.contextPath = contextPath;
    this.documentBuilderFactory = DocumentBuilderFactory.newInstance();
  }

  public static void processConfigResource(InputStream is, JavascriptConfigService service, ServletContext scontext) {
    JavascriptConfigParser parser = new JavascriptConfigParser(scontext.getContextPath());
    LinkedList<ScriptResourceDescriptor> descriptors = new LinkedList<>();
    JavascriptTask task = new JavascriptTask();
    for (ScriptResourceDescriptor script : parser.parseConfig(is)) {
      task.addDescriptor(script);
      descriptors.add(script);
    }
    scontext.setAttribute(SCRIPT_RESOURCE_DESCRIPTORS_ATTR, Collections.unmodifiableList(descriptors));
    task.execute(service, scontext);
  }

  public static void unregisterResources(JavascriptConfigService service, ServletContext scontext) {
    @SuppressWarnings("unchecked")
    List<ScriptResourceDescriptor> descriptors =
                                               (List<ScriptResourceDescriptor>) scontext.getAttribute(SCRIPT_RESOURCE_DESCRIPTORS_ATTR);
    if (descriptors == null)
      return;

    JavascriptUnregisterTask task = new JavascriptUnregisterTask();
    for (ScriptResourceDescriptor script : descriptors) {
      task.addDescriptor(script);
    }
    task.execute(service, scontext);
    scontext.removeAttribute(SCRIPT_RESOURCE_DESCRIPTORS_ATTR);
  }

  public List<ScriptResourceDescriptor> parseConfig(InputStream is) {
    try {
      DocumentBuilder docBuilder = documentBuilderFactory.newDocumentBuilder();
      Document document = docBuilder.parse(is);
      return parseScripts(document);
    } catch (Exception e) {
      LOG.error(e.getMessage(), e);
      return null;
    }
  }

  private List<ScriptResourceDescriptor> parseScripts(Document document) {
    List<ScriptResourceDescriptor> tasks = new ArrayList<>();
    Element element = document.getDocumentElement();
    for (String tagName : Arrays.asList(JAVA_SCRIPT_TAG, MODULE_TAG, SCRIPTS_TAG, PORTLET_TAG, PORTAL_TAG)) {
      for (Element childElt : XMLTools.getChildren(element, tagName)) {
        Collection<ScriptResourceDescriptor> task = parseScripts(childElt);
        if (task != null) {
          tasks.addAll(task);
        }
      }
    }
    return tasks;
  }

  private Collection<ScriptResourceDescriptor> parseScripts(Element element) {
    LinkedHashMap<ResourceId, ScriptResourceDescriptor> scripts = new LinkedHashMap<ResourceId, ScriptResourceDescriptor>();
    if (JAVA_SCRIPT_TAG.equals(element.getTagName())) {
      try {
        NodeList nodes = element.getElementsByTagName(JAVA_SCRIPT_PARAM);
        int length = nodes.getLength();
        for (int i = 0; i < length; i++) {
          Element param_ele = (Element) nodes.item(i);
          String js_path = param_ele.getElementsByTagName(JAVA_SCRIPT_PATH).item(0).getFirstChild().getNodeValue();

          //
          int priority;
          try {
            priority = Integer.valueOf(
                                       param_ele.getElementsByTagName(JAVA_SCRIPT_PRIORITY)
                                                .item(0)
                                                .getFirstChild()
                                                .getNodeValue())
                              .intValue();
          } catch (Exception e) {
            priority = Integer.MAX_VALUE;
          }
          String portalName = null;
          try {
            portalName = param_ele.getElementsByTagName(JAVA_SCRIPT_PORTAL_NAME)
                                  .item(0)
                                  .getFirstChild()
                                  .getNodeValue();
          } catch (Exception e) {
            // portal-name is null
          }

          Javascript js;
          if (portalName == null) {
            js = Javascript.create(new ResourceId(ResourceScope.SHARED, LEGACY_JAVA_SCRIPT),
                                   js_path,
                                   contextPath,
                                   priority);
          } else {
            js = Javascript
                           .create(new ResourceId(ResourceScope.PORTAL, portalName), js_path, contextPath, priority);
          }

          //
          ScriptResourceDescriptor desc = scripts.get(js.getResource());
          if (desc == null) {
            scripts.put(js.getResource(),
                        desc = new ScriptResourceDescriptor(js.getResource(), FetchMode.IMMEDIATE));
          }
          desc.modules.add(js);
        }
      } catch (Exception ex) {
        LOG.error(ex.getMessage(), ex);
      }
    } else if (PORTAL_TAG.equals(element.getTagName()) || PORTLET_TAG.equals(element.getTagName())) {
      String resourceName = XMLTools.asString(XMLTools.getUniqueChild(element, "name", true));
      ResourceScope resourceScope;
      if (PORTLET_TAG.equals(element.getTagName())) {
        resourceName = contextPath.substring(1) + "/" + resourceName;
        resourceScope = ResourceScope.PORTLET;
      } else {
        resourceScope = ResourceScope.PORTAL;
      }
      ResourceId id = new ResourceId(resourceScope, resourceName);
      FetchMode fetchMode;
      String group = null;

      Element resourceElt = XMLTools.getUniqueChild(element, MODULE_TAG, false);
      if (resourceElt != null) {
        fetchMode = FetchMode.ON_LOAD;
        if (XMLTools.getUniqueChild(resourceElt, URL_TAG, false) == null) {
          group = parseGroup(resourceElt);
        }
      } else {
        resourceElt = XMLTools.getUniqueChild(element, SCRIPTS_TAG, false);
        fetchMode = FetchMode.IMMEDIATE;
      }

      if (resourceElt != null) {
        ScriptResourceDescriptor desc = scripts.get(id);
        if (desc == null) {
          desc = new ScriptResourceDescriptor(id, fetchMode, parseOptString(element, AS_TAG), group);
        } else {
          desc.fetchMode = fetchMode;
        }
        parseDesc(resourceElt, desc);
        scripts.put(id, desc);
      }
    } else if (MODULE_TAG.equals(element.getTagName()) || SCRIPTS_TAG.equals(element.getTagName())) {
      String resourceName = XMLTools.asString(XMLTools.getUniqueChild(element, "name", true));
      ResourceId id = null;
      if (resourceName.contains("/")) {
        String[] resourceParts = resourceName.split("/");
        id = new ResourceId(ResourceScope.valueOf(resourceParts[0].toUpperCase()), resourceParts[1]);
      } else {
        id = new ResourceId(ResourceScope.SHARED, resourceName);
      }

      FetchMode fetchMode;
      String group = null;

      if (MODULE_TAG.equals(element.getTagName())) {
        fetchMode = FetchMode.ON_LOAD;
        if (XMLTools.getUniqueChild(element, URL_TAG, false) == null) {
          group = parseGroup(element);
        }
      } else {
        fetchMode = FetchMode.IMMEDIATE;
      }

      ScriptResourceDescriptor desc = scripts.get(id);
      if (desc == null) {
        desc = new ScriptResourceDescriptor(id, fetchMode, parseOptString(element, AS_TAG), group);
      }
      parseDesc(element, desc);
      scripts.put(id, desc);
    } else {
      // ???
    }

    //
    return scripts.values();
  }

  private void parseDesc(Element element, ScriptResourceDescriptor desc) {
    Element urlElement = XMLTools.getUniqueChild(element, URL_TAG, false);
    if (urlElement != null) {
      String remoteURL = XMLTools.asString(urlElement);
      desc.id.setFullId(false);
      desc.modules.add(new Javascript.Remote(desc.id, contextPath, remoteURL, 0));
    } else {
      for (Element localeElt : XMLTools.getChildren(element, "supported-locale")) {
        String localeValue = XMLTools.asString(localeElt);
        Locale locale = I18N.parseTagIdentifier(localeValue);
        desc.supportedLocales.add(locale);
      }
      for (Element scriptElt : XMLTools.getChildren(element, SCRIPT_TAG)) {
        String resourceBundle = parseOptString(scriptElt, "resource-bundle");

        String minifyStr = parseOptString(scriptElt, "minify");
        boolean minify = minifyStr == null || Boolean.parseBoolean(minifyStr);

        List<Content> contents = new LinkedList<Content>();
        Element adapter = XMLTools.getUniqueChild(scriptElt, ADAPTER_TAG, false);
        String scriptPath = parseOptString(scriptElt, "path");
        if (scriptPath != null) {
          contents.add(new Content(scriptPath));
        } else if (adapter != null) {
          NodeList childs = adapter.getChildNodes();
          for (int i = 0; i < childs.getLength(); i++) {
            Node item = childs.item(i);
            if (item instanceof Element) {
              Element include = (Element) item;
              if (INCLUDE_TAG.equals(include.getTagName())) {
                contents.add(new Content(XMLTools.asString(include, true)));
              }
            } else if (item.getNodeType() == Node.TEXT_NODE) {
              contents.add(new Content(item.getNodeValue().trim(), false));
            }
          }
        }
        Content[] tmp = contents.toArray(new Content[contents.size()]);

        //
        Javascript script = new Javascript.Local(desc.id, contextPath, tmp, resourceBundle, 0, minify);
        desc.modules.add(script);
      }
    }
    for (Element moduleElt : XMLTools.getChildren(element, "depends")) {
      Element dependencyElt = XMLTools.getUniqueChild(moduleElt, "module", false);
      if (dependencyElt == null) {
        dependencyElt = XMLTools.getUniqueChild(moduleElt, "scripts", false);
      }
      ResourceId resourceId = new ResourceId(ResourceScope.SHARED, XMLTools.asString(dependencyElt));
      DependencyDescriptor dependency = new DependencyDescriptor(resourceId,
                                                                 parseOptString(moduleElt, AS_TAG),
                                                                 parseOptString(moduleElt, RESOURCE_TAG));
      desc.dependencies.add(dependency);
    }
  }

  private String parseGroup(Element element) {
    Element group = XMLTools.getUniqueChild(element, GROUP_TAG, false);
    if (group != null) {
      String grpName = XMLTools.asString(group, true);
      if (grpName.isEmpty()) {
        grpName = null;
      }
      return grpName;
    } else {
      return null;
    }
  }

  private String parseOptString(Element element, String childTag) {
    Element childElt = XMLTools.getUniqueChild(element, childTag, false);
    return childElt == null ? null : XMLTools.asString(childElt, true);
  }
}
