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

import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.lang.invoke.MethodHandles;
import java.util.HashMap;

import org.apache.commons.lang3.StringUtils;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.io.Text;
import org.apache.nutch.metadata.HttpHeaders;
import org.apache.nutch.util.MimeUtil;
import org.apache.nutch.util.NutchConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Extension of @see AdaptiveFetchSchedule that allows for more flexible
 * configuration of DEC and INC factors for various MIME-types.
 * 
 * This class can be typically used in cases where a recrawl consists of many
 * different MIME-types. It's not very common for MIME-types other than
 * text/html to change frequently. Using this class you can configure different
 * factors per MIME-type so to prefer frequently changing MIME-types over
 * others.
 * 
 * For it to work this class relies on the Content-Type MetaData key being
 * present in the CrawlDB. This can either be done when injecting new URL's or
 * by adding "Content-Type" to the db.parsemeta.to.crawldb configuration setting
 * to force MIME-types of newly discovered URL's to be added to the CrawlDB.
 * 
 * @author markus
 */
public class MimeAdaptiveFetchSchedule extends AdaptiveFetchSchedule {
  // Loggg
  private static final Logger LOG = LoggerFactory
      .getLogger(MethodHandles.lookup().lookupClass());

  // Conf directives
  public static final String SCHEDULE_INC_RATE = "db.fetch.schedule.adaptive.inc_rate";
  public static final String SCHEDULE_DEC_RATE = "db.fetch.schedule.adaptive.dec_rate";
  public static final String SCHEDULE_MIME_FILE = "db.fetch.schedule.mime.file";

  // Default values for DEC and INC rate
  private float defaultIncRate;
  private float defaultDecRate;

  // Structure to store inc and dec rates per MIME-type
  private class AdaptiveRate {
    public float inc;
    public float dec;

    public AdaptiveRate(Float inc, Float dec) {
      this.inc = inc;
      this.dec = dec;
    }
  }

  // Here we store the mime's and their delta's
  private HashMap<String, AdaptiveRate> mimeMap;

  @Override
  public void setConf(Configuration conf) {
    super.setConf(conf);
    if (conf == null)
      return;

    // Read and set the default INC and DEC rates in case we cannot set values
    // based on MIME-type
    defaultIncRate = conf.getFloat(SCHEDULE_INC_RATE, 0.2f);
    defaultDecRate = conf.getFloat(SCHEDULE_DEC_RATE, 0.2f);

    // Where's the mime/factor file?
    Reader mimeFile = conf.getConfResourceAsReader(conf.get(SCHEDULE_MIME_FILE,
        "adaptive-mimetypes.txt"));

    try {
      readMimeFile(mimeFile);
    } catch (IOException e) {
      LOG.error(org.apache.hadoop.util.StringUtils.stringifyException(e));
    }
  }

  @Override
  public CrawlDatum setFetchSchedule(Text url, CrawlDatum datum,
      long prevFetchTime, long prevModifiedTime, long fetchTime,
      long modifiedTime, int state) {

    // Set defaults
    INC_RATE = defaultIncRate;
    DEC_RATE = defaultDecRate;

    // Check if the Content-Type field is available in the CrawlDatum
    if (datum.getMetaData().containsKey(HttpHeaders.WRITABLE_CONTENT_TYPE)) {
      // Get the MIME-type of the current URL
      String currentMime = MimeUtil.cleanMimeType(datum.getMetaData()
          .get(HttpHeaders.WRITABLE_CONTENT_TYPE).toString());

      // Check if this MIME-type exists in our map
      if (mimeMap.containsKey(currentMime)) {
        // Yes, set the INC and DEC rates for this MIME-type
        INC_RATE = mimeMap.get(currentMime).inc;
        DEC_RATE = mimeMap.get(currentMime).dec;
      }
    }

    return super.setFetchSchedule(url, datum, prevFetchTime, prevModifiedTime,
        fetchTime, modifiedTime, state);
  }

  /**
   * Reads the mime types and their associated INC/DEC factors in a HashMap
   * 
   * @param mimeFile
   *          Reader
   * @return void
   */
  private void readMimeFile(Reader mimeFile) throws IOException {
    // Instance of our mime/factor map
    mimeMap = new HashMap<>();

    // Open a reader
    BufferedReader reader = new BufferedReader(mimeFile);

    String line = null;
    String[] splits = null;

    // Read all lines
    while ((line = reader.readLine()) != null) {
      // Skip blank lines and comments
      if (StringUtils.isNotBlank(line) && !line.startsWith("#")) {
        // Split the line by TAB
        splits = line.split("\t");

        // Sanity check, we need two or three items
        if (splits.length == 3) {
          // Add a lower cased MIME-type and the factor to the map
          mimeMap.put(StringUtils.lowerCase(splits[0]), new AdaptiveRate(
              Float.valueOf(splits[1]), Float.valueOf(splits[2])));
        } else {
          LOG.warn("Invalid configuration line in: {}", line);
        }
      }
    }
  }

  public static void main(String[] args) throws Exception {
    FetchSchedule fs = new MimeAdaptiveFetchSchedule();
    fs.setConf(NutchConfiguration.create());
    // we start the time at 0, for simplicity
    long curTime = 0;
    long delta = 1000L * 3600L * 24L; // 2 hours
    // we trigger the update of the page every 30 days
    long update = 1000L * 3600L * 24L * 30L; // 30 days
    boolean changed = true;
    long lastModified = 0;
    int miss = 0;
    int totalMiss = 0;
    int maxMiss = 0;
    int fetchCnt = 0;
    int changeCnt = 0;

    // initial fetchInterval is 10 days
    CrawlDatum p = new CrawlDatum(1, 3600 * 24 * 30, 1.0f);

    // Set a default MIME-type to test with
    org.apache.hadoop.io.MapWritable x = new org.apache.hadoop.io.MapWritable();
    x.put(HttpHeaders.WRITABLE_CONTENT_TYPE, new Text(
        "text/html; charset=utf-8"));
    p.setMetaData(x);

    p.setFetchTime(0);
    LOG.info(p.toString());

    // let's move the timeline a couple of deltas
    for (int i = 0; i < 10000; i++) {
      if (lastModified + update < curTime) {
        // System.out.println("i=" + i + ", lastModified=" + lastModified +
        // ", update=" + update + ", curTime=" + curTime);
        changed = true;
        changeCnt++;
        lastModified = curTime;
      }

      LOG.info("{}. {}\twill fetch at {}\tinterval {} days\t missed {}", i,
          changed, (p.getFetchTime() / delta),
          (p.getFetchInterval() / SECONDS_PER_DAY), miss);

      if (p.getFetchTime() <= curTime) {
        fetchCnt++;
        fs.setFetchSchedule(new Text("http://www.example.com"), p, p
            .getFetchTime(), p.getModifiedTime(), curTime, lastModified,
            changed ? FetchSchedule.STATUS_MODIFIED
                : FetchSchedule.STATUS_NOTMODIFIED);

        LOG.info(
            "\tfetched and adjusted:\twill fetch at {}\tinterval {} days",
            (p.getFetchTime() / delta),
            (p.getFetchInterval() / SECONDS_PER_DAY));

        if (!changed)
          miss++;
        if (miss > maxMiss)
          maxMiss = miss;
        changed = false;
        totalMiss += miss;
        miss = 0;
      }

      if (changed)
        miss++;
      curTime += delta;
    }
    LOG.info("Total missed: {}, max miss: {}", totalMiss, maxMiss);
    LOG.info("Page changed {} times, fetched {} times.", changeCnt, fetchCnt);
  }

}
