/*
 * 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.nutch.protocol;

import java.io.BufferedReader;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.net.URL;
import java.net.MalformedURLException;
import java.util.HashMap;
import java.util.Map;

import org.apache.nutch.plugin.Extension;
import org.apache.nutch.plugin.ExtensionPoint;
import org.apache.nutch.plugin.PluginRepository;
import org.apache.nutch.plugin.PluginRuntimeException;
import org.apache.nutch.util.ObjectCache;
import org.apache.nutch.util.URLUtil;

import org.apache.commons.lang3.StringUtils;

import org.apache.hadoop.conf.Configuration;

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

/**
 * Creates and caches {@link Protocol} plugins. Protocol plugins should define
 * the attribute "protocolName" with the name of the protocol that they
 * implement. Configuration object is used for caching. Cache key is constructed
 * from appending protocol name (eg. http) to constant
 * {@link Protocol#X_POINT_ID}.
 */
public class ProtocolFactory {

  private static final Logger LOG = LoggerFactory
      .getLogger(MethodHandles.lookup().lookupClass());

  private ExtensionPoint extensionPoint;

  private Configuration conf;

  protected Map<String, String> defaultProtocolImplMapping = new HashMap<>();
  protected Map<String, String> hostProtocolMapping = new HashMap<>();

  public ProtocolFactory(Configuration conf) {
    this.conf = conf;
    this.extensionPoint = PluginRepository.get(conf).getExtensionPoint(
        Protocol.X_POINT_ID);
    if (this.extensionPoint == null) {
      throw new RuntimeException("x-point " + Protocol.X_POINT_ID
          + " not found.");
    }

    try {
      BufferedReader reader = new BufferedReader(conf.getConfResourceAsReader("host-protocol-mapping.txt"));
      String line;
      String parts[];
      while ((line = reader.readLine()) != null) {
        if (StringUtils.isNotBlank(line) && !line.startsWith("#")) {
          line = line.trim();
          parts = line.split("\t");

          // Must be at least two parts
          if (parts.length == 2) {
            // Is this a host to plugin mapping, or a default?
            if (parts[0].indexOf(":") == -1) {
              hostProtocolMapping.put(parts[0].trim(), parts[1].trim());
            } else {
              String[] moreParts = parts[0].split(":");
              defaultProtocolImplMapping.put(moreParts[1].trim(), parts[1].trim());
            }
          } else {
            LOG.warn("Wrong format of line: {}", line);
            LOG.warn("Expected format: <hostname> <tab> <plugin_id> or protocol:<protocol> <tab> <plugin_id>");
          }
        }
      }
    } catch (IOException e) {
      LOG.error("Unable to read host-protocol-mapping.txt", e);
    }
  }
  /**
   * Returns the appropriate {@link Protocol} implementation for a url.
   * 
   * @param urlString
   *          Url String
   * @return The appropriate {@link Protocol} implementation for a given
   *         {@link URL}.
   * @throws ProtocolNotFound
   *           when Protocol can not be found for urlString or urlString is not
   *           a valid URL
   */
  public Protocol getProtocol(String urlString) throws ProtocolNotFound {
    try {
      URL url = new URL(urlString);
      return getProtocol(url);
    } catch (MalformedURLException e) {
      throw new ProtocolNotFound(urlString, e.toString());
    }
  }

  /**
   * Returns the appropriate {@link Protocol} implementation for a url.
   * 
   * @param url
   *          URL to be fetched by returned {@link Protocol} implementation
   * @return The appropriate {@link Protocol} implementation for a given
   *         {@link URL}.
   * @throws ProtocolNotFound
   *           when Protocol can not be found for url
   */
  public Protocol getProtocol(URL url)
      throws ProtocolNotFound {
    try {
      Protocol protocol = null;

      // First attempt to resolve a protocol implementation by hostname
      String host = url.getHost();
      String domain = URLUtil.getDomainName(url).toLowerCase().trim();
      String hostOrDomain = null;
      Extension extension = null;
      if (hostProtocolMapping.containsKey(host)) {
        hostOrDomain = host;
      } else if (hostProtocolMapping.containsKey(domain)) {
        hostOrDomain = domain;
      }
      if (hostOrDomain != null) {
        extension = getExtensionById(hostProtocolMapping.get(hostOrDomain));
        if (extension != null) {
          protocol = getProtocolInstanceByExtension(extension);
        }
      }

      // Nothing, see if we have defaults configured
      if (protocol == null) {
        // Protocol listed in default map?
        if (defaultProtocolImplMapping.containsKey(url.getProtocol())) {
          extension = getExtensionById(defaultProtocolImplMapping.get(url.getProtocol()));
          if (extension != null) {
            protocol = getProtocolInstanceByExtension(extension);
          }
        }
      }

      // Still couldn't find a protocol? Attempt by protocol
      if (protocol == null) {
        extension = findExtension(url.getProtocol(), "protocolName");
        if (extension != null) {
          protocol = getProtocolInstanceByExtension(extension);
        }
      }

      // Got anything?
      if (protocol != null) {
        return protocol;
      }

      // Nothing!
      throw new ProtocolNotFound(url.toString());
    } catch (PluginRuntimeException e) {
      throw new ProtocolNotFound(url.toString(), e.toString());
    }
  }

  private Protocol getProtocolInstanceByExtension(Extension extension) throws PluginRuntimeException {
    Protocol protocol = null;
    String cacheId = extension.getId();
    ObjectCache objectCache = ObjectCache.get(conf);
    synchronized (objectCache) {
      if (!objectCache.hasObject(cacheId)) {
        protocol = (Protocol) extension.getExtensionInstance();
        objectCache.setObject(cacheId, protocol);
      }
      protocol = (Protocol) objectCache.getObject(cacheId);
    }

    return protocol;
  }

  private Extension getExtensionById(String id) {
    Extension[] extensions = this.extensionPoint.getExtensions();
    for (int i = 0; i < extensions.length; i++) {
      if (id.equals(extensions[i].getId())) {
        return extensions[i];
      }
    }

    return null;
  }

  private Extension findExtension(String name, String attribute) throws PluginRuntimeException {
    for (int i = 0; i < this.extensionPoint.getExtensions().length; i++) {
      Extension extension = this.extensionPoint.getExtensions()[i];

      if (contains(name, extension.getAttribute(attribute)))
        return extension;
    }
    return null;
  }

  boolean contains(String what, String where) {
    if (where != null) {
      String parts[] = where.split("[, ]");
      for (int i = 0; i < parts.length; i++) {
        if (parts[i].equals(what))
          return true;
      }
    }
    return false;
  }

  /** Get a {@link Protocol} instance of the specified extension ID. */
  /**
   * @param id
   *          protocol plugin ID, e.g.,
   *          <code>org.apache.nutch.protocol.http</code>
   * @return protocol instance for the given ID
   * @throws PluginRuntimeException
   *           if plugin not found or failed to instantiate
   */
  public Protocol getProtocolById(String id) throws PluginRuntimeException {
    Extension ext = getExtensionById(id);
    if (ext == null) {
      throw new PluginRuntimeException("ID " + id + " not found");
    }
    return getProtocolInstanceByExtension(ext);
  }
}
