/**
 * 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
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * 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.hadoop.hdfs.util;

import org.apache.hadoop.io.erasurecode.ECSchema;
import org.apache.hadoop.classification.InterfaceAudience;
import org.apache.hadoop.hdfs.protocol.ErasureCodingPolicy;
import org.w3c.dom.Node;
import org.w3c.dom.Text;
import org.w3c.dom.Element;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Map;
import java.util.List;
import java.util.HashMap;
import java.util.ArrayList;
import java.util.Collections;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A EC policy loading tool that loads user defined EC policies from XML file.
 */
@InterfaceAudience.Private
public class ECPolicyLoader {

  private static final Logger LOG
      = LoggerFactory.getLogger(ECPolicyLoader.class);

  private static final int LAYOUT_VERSION = 1;

  /**
   * Load user defined EC policies from a XML configuration file.
   * @param policyFilePath path of EC policy file
   * @return all valid EC policies in EC policy file
   */
  public List<ErasureCodingPolicy> loadPolicy(String policyFilePath) {
    try {
      File policyFile = getPolicyFile(policyFilePath);
      if (!policyFile.exists()) {
        LOG.warn("Not found any EC policy file");
        return Collections.emptyList();
      }
      return loadECPolicies(policyFile);
    } catch (ParserConfigurationException | IOException | SAXException e) {
      throw new RuntimeException("Failed to load EC policy file: "
          + policyFilePath);
    }
  }

  /**
   * Load EC policies from a XML configuration file.
   * @param policyFile EC policy file
   * @return list of EC policies
   * @throws ParserConfigurationException if ParserConfigurationException happen
   * @throws IOException if no such EC policy file
   * @throws SAXException if the xml file has some invalid elements
   */
  private List<ErasureCodingPolicy> loadECPolicies(File policyFile)
      throws ParserConfigurationException, IOException, SAXException {

    LOG.info("Loading EC policy file " + policyFile);

    // Read and parse the EC policy file.
    DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
    dbf.setIgnoringComments(true);
    DocumentBuilder builder = dbf.newDocumentBuilder();
    Document doc = builder.parse(policyFile);
    Element root = doc.getDocumentElement();

    if (!"configuration".equals(root.getTagName())) {
      throw new RuntimeException("Bad EC policy configuration file: "
          + "top-level element not <configuration>");
    }

    List<ErasureCodingPolicy> policies;
    if (root.getElementsByTagName("layoutversion").getLength() > 0) {
      if (loadLayoutVersion(root) == LAYOUT_VERSION) {
        if (root.getElementsByTagName("schemas").getLength() > 0) {
          Map<String, ECSchema> schemas = loadSchemas(root);
          if (root.getElementsByTagName("policies").getLength() > 0) {
            policies = loadPolicies(root, schemas);
          } else {
            throw new RuntimeException("Bad EC policy configuration file: "
                + "no <policies> element");
          }
        } else {
          throw new RuntimeException("Bad EC policy configuration file: "
              + "no <schemas> element");
        }
      } else {
        throw new RuntimeException("The parse failed because of "
            + "bad layoutversion value");
      }
    } else {
      throw new RuntimeException("Bad EC policy configuration file: "
          + "no <layoutVersion> element");
    }

    return policies;
  }

  /**
   * Load layoutVersion from root element in the XML configuration file.
   * @param root root element
   * @return layout version
   */
  private int loadLayoutVersion(Element root) {
    int layoutVersion;
    Text text = (Text) root.getElementsByTagName("layoutversion")
        .item(0).getFirstChild();
    if (text != null) {
      String value = text.getData().trim();
      try {
        layoutVersion = Integer.parseInt(value);
      } catch (NumberFormatException e) {
        throw new IllegalArgumentException("Bad layoutVersion value "
            + value + " is found. It should be an integer");
      }
    } else {
      throw new IllegalArgumentException("Value of <layoutVersion> is null");
    }

    return layoutVersion;
  }

  /**
   * Load schemas from root element in the XML configuration file.
   * @param root root element
   * @return EC schema map
   */
  private Map<String, ECSchema> loadSchemas(Element root) {
    NodeList elements = root.getElementsByTagName("schemas")
        .item(0).getChildNodes();
    Map<String, ECSchema> schemas = new HashMap<String, ECSchema>();
    for (int i = 0; i < elements.getLength(); i++) {
      Node node = elements.item(i);
      if (node instanceof Element) {
        Element element = (Element) node;
        if ("schema".equals(element.getTagName())) {
          String schemaId = element.getAttribute("id");
          ECSchema schema = loadSchema(element);
          if (!schemas.containsValue(schema)) {
            schemas.put(schemaId, schema);
          } else {
            throw new RuntimeException("Repetitive schemas in EC policy"
                + " configuration file: " + schemaId);
          }
        } else {
          throw new RuntimeException("Bad element in EC policy"
              + " configuration file: " + element.getTagName());
        }
      }
    }

    return schemas;
  }

  /**
   * Load EC policies from root element in the XML configuration file.
   * @param root root element
   * @param schemas schema map
   * @return EC policy list
   */
  private List<ErasureCodingPolicy> loadPolicies(
      Element root, Map<String, ECSchema> schemas) {
    NodeList elements = root.getElementsByTagName("policies")
        .item(0).getChildNodes();
    List<ErasureCodingPolicy> policies = new ArrayList<ErasureCodingPolicy>();
    for (int i = 0; i < elements.getLength(); i++) {
      Node node = elements.item(i);
      if (node instanceof Element) {
        Element element = (Element) node;
        if ("policy".equals(element.getTagName())) {
          ErasureCodingPolicy policy = loadPolicy(element, schemas);
          if (!policies.contains(policy)) {
            policies.add(policy);
          } else {
            LOG.warn("Repetitive policies in EC policy configuration file: "
                + policy.toString());
          }
        } else {
          throw new RuntimeException("Bad element in EC policy configuration"
              + " file: " + element.getTagName());
        }
      }
    }

    return policies;
  }

  /**
   * Path to the XML file containing user defined EC policies. If the path is
   * relative, it is searched for in the classpath.
   * @param policyFilePath path of EC policy file
   * @return EC policy file
   */
  private File getPolicyFile(String policyFilePath)
      throws MalformedURLException {
    File policyFile = new File(policyFilePath);
    if (!policyFile.isAbsolute()) {
      URL url = new URL(policyFilePath);
      if (!url.getProtocol().equalsIgnoreCase("file")) {
        throw new RuntimeException(
            "EC policy file " + url
                + " found on the classpath is not on the local filesystem.");
      } else {
        policyFile = new File(url.getPath());
      }
    }

    return policyFile;
  }

  /**
   * Load a schema from a schema element in the XML configuration file.
   * @param element EC schema element
   * @return ECSchema
   */
  private ECSchema loadSchema(Element element) {
    Map<String, String> schemaOptions = new HashMap<String, String>();
    NodeList fields = element.getChildNodes();

    for (int i = 0; i < fields.getLength(); i++) {
      Node fieldNode = fields.item(i);
      if (fieldNode instanceof Element) {
        Element field = (Element) fieldNode;
        String tagName = field.getTagName();
        if ("k".equals(tagName)) {
          tagName = "numDataUnits";
        } else if ("m".equals(tagName)) {
          tagName = "numParityUnits";
        }

        // Get the nonnull text value.
        Text text = (Text) field.getFirstChild();
        if (text != null) {
          String value = text.getData().trim();
          schemaOptions.put(tagName, value);
        } else {
          throw new IllegalArgumentException("Value of <" + tagName
              + "> is null");
        }
      }
    }

    return new ECSchema(schemaOptions);
  }

  /**
   * Load a EC policy from a policy element in the XML configuration file.
   * @param element EC policy element
   * @param schemas all valid schemas of the EC policy file
   * @return EC policy
   */
  private ErasureCodingPolicy loadPolicy(Element element,
                                         Map<String, ECSchema> schemas) {
    NodeList fields = element.getChildNodes();
    ECSchema schema = null;
    int cellSize = 0;

    for (int i = 0; i < fields.getLength(); i++) {
      Node fieldNode = fields.item(i);
      if (fieldNode instanceof Element) {
        Element field = (Element) fieldNode;
        String tagName = field.getTagName();

        // Get the nonnull text value.
        Text text = (Text) field.getFirstChild();
        if (text != null) {
          if (!text.isElementContentWhitespace()) {
            String value = text.getData().trim();
            if ("schema".equals(tagName)) {
              schema = schemas.get(value);
            } else if ("cellsize".equals(tagName)) {
              try {
                cellSize = Integer.parseInt(value);
              } catch (NumberFormatException e) {
                throw new IllegalArgumentException("Bad EC policy cellsize"
                    + " value " + value + " is found. It should be an integer");
              }
            } else {
              LOG.warn("Invalid tagName: " + tagName);
            }
          }
        } else {
          throw new IllegalArgumentException("Value of <" + tagName
              + "> is null");
        }
      }
    }

    if (schema != null && cellSize > 0) {
      return new ErasureCodingPolicy(schema, cellSize, (byte) -1);
    } else {
      throw new RuntimeException("Bad policy is found in"
          + " EC policy configuration file");
    }
  }
}
