/*
 * Copyright (C) 2015 - present by OpenGamma Inc. and the OpenGamma group of companies
 *
 * Please see distribution for license.
 */
package com.opengamma.strata.loader.csv;

import static com.opengamma.strata.collect.Guavate.toImmutableList;
import static java.util.stream.Collectors.toList;

import java.time.LocalDate;
import java.time.YearMonth;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import com.google.common.collect.ImmutableMap;
import com.google.common.io.CharSource;
import com.opengamma.strata.basics.index.Index;
import com.opengamma.strata.basics.index.PriceIndex;
import com.opengamma.strata.collect.MapStream;
import com.opengamma.strata.collect.io.CharSources;
import com.opengamma.strata.collect.io.CsvFile;
import com.opengamma.strata.collect.io.CsvRow;
import com.opengamma.strata.collect.io.ResourceLocator;
import com.opengamma.strata.collect.io.UnicodeBom;
import com.opengamma.strata.collect.result.ParseFailureException;
import com.opengamma.strata.collect.timeseries.LocalDateDoubleTimeSeries;
import com.opengamma.strata.collect.timeseries.LocalDateDoubleTimeSeriesBuilder;
import com.opengamma.strata.data.ObservableId;
import com.opengamma.strata.loader.LoaderUtils;
import com.opengamma.strata.market.observable.IndexQuoteId;

/**
 * Loads a set of historical fixing series into memory from CSV resources.
 * <p>
 * The resources are expected to be in a CSV format, with the following header row:<br />
 * {@code Reference, Date, Value}.
 * <ul>
 * <li>The 'Reference' column is the name of the index that the data is for, such as 'USD-LIBOR-3M'.
 * <li>The 'Date' column is the date that the fixing was taken, this should be a year-month for price indices.
 * <li>The 'Value' column is the fixed value.
 * </ul>
 * <p>
 * Each fixing series must be contained entirely within a single resource, but each resource may
 * contain more than one series. The fixing series points do not need to be ordered.
 * <p>
 * For example:
 * <pre>
 * Reference, Date, Value
 * USD-LIBOR-3M, 1971-01-04, 0.065
 * USD-LIBOR-3M, 1971-01-05, 0.0638
 * USD-LIBOR-3M, 1971-01-06, 0.0638
 * </pre>
 * Note that Microsoft Excel prefers the CSV file to have no space after the comma.
 * <p>
 * CSV files sometimes contain a Unicode Byte Order Mark.
 * Callers are responsible for handling this, such as by using {@link UnicodeBom}.
 */
public final class FixingSeriesCsvLoader {

  // CSV column headers
  private static final String REFERENCE_FIELD = "Reference";
  private static final String DATE_FIELD = "Date";
  private static final String VALUE_FIELD = "Value";

  //-------------------------------------------------------------------------
  /**
   * Loads one or more CSV format fixing series files.
   * <p>
   * If the files contain a duplicate entry an exception will be thrown.
   * 
   * @param resources  the fixing series CSV resources
   * @return the loaded fixing series, mapped by {@linkplain ObservableId observable ID}
   * @throws IllegalArgumentException if the files contain a duplicate entry
   */
  public static ImmutableMap<ObservableId, LocalDateDoubleTimeSeries> load(ResourceLocator... resources) {
    return load(Arrays.asList(resources));
  }

  /**
   * Loads one or more CSV format fixing series files.
   * <p>
   * If the files contain a duplicate entry an exception will be thrown.
   * 
   * @param resources  the fixing series CSV resources
   * @return the loaded fixing series, mapped by {@linkplain ObservableId observable ID}
   * @throws IllegalArgumentException if the files contain a duplicate entry
   */
  public static ImmutableMap<ObservableId, LocalDateDoubleTimeSeries> load(Collection<ResourceLocator> resources) {
    Collection<CharSource> charSources = resources.stream().map(r -> r.getCharSource()).collect(toList());
    return parse(charSources);
  }

  //-------------------------------------------------------------------------
  /**
   * Parses one or more CSV format fixing series files.
   * <p>
   * If the files contain a duplicate entry an exception will be thrown.
   * 
   * @param charSources  the fixing series CSV character sources
   * @return the loaded fixing series, mapped by {@linkplain ObservableId observable ID}
   * @throws IllegalArgumentException if the files contain a duplicate entry
   */
  public static ImmutableMap<ObservableId, LocalDateDoubleTimeSeries> parse(Collection<CharSource> charSources) {
    // builder ensures keys can only be seen once
    try {
      ImmutableMap.Builder<ObservableId, LocalDateDoubleTimeSeries> builder = ImmutableMap.builder();
      for (CharSource charSource : charSources) {
        builder.putAll(parseSingle(charSource));
      }
      return builder.build();
    } catch (ParseFailureException ex) {
      throw ex;
    } catch (RuntimeException ex) {
      throw new ParseFailureException(
          ex, 
          "Error parsing CSV files '{fileName}': {exceptionMessage}",
          charSources.stream().map(source -> CharSources.extractFileName(source)).collect(toImmutableList()),
          ex.getMessage());
    }
  }

  //-------------------------------------------------------------------------
  // loads a single fixing series CSV file
  private static ImmutableMap<ObservableId, LocalDateDoubleTimeSeries> parseSingle(CharSource resource) {
    Map<ObservableId, LocalDateDoubleTimeSeriesBuilder> builders = new HashMap<>();
    try {
      CsvFile csv = CsvFile.of(resource, true);
      for (CsvRow row : csv.rows()) {
        String referenceStr = row.getField(REFERENCE_FIELD);
        String dateStr = row.getField(DATE_FIELD);
        String valueStr = row.getField(VALUE_FIELD);

        Index index = LoaderUtils.findIndex(referenceStr);
        ObservableId id = IndexQuoteId.of(index);
        double value = Double.parseDouble(valueStr);
        LocalDate date;
        if (index instanceof PriceIndex) {
          try {
            YearMonth ym = LoaderUtils.parseYearMonth(dateStr);
            date = ym.atEndOfMonth();
          } catch (RuntimeException ex) {
            date = LoaderUtils.parseDate(dateStr);
            if (date.getDayOfMonth() != date.lengthOfMonth()) {
              throw new ParseFailureException(
                  "Unable to parse price index from '{fileName}', must have date at end of month",
                  CharSources.extractFileName(resource));
            }
          }
        } else {
          date = LoaderUtils.parseDate(dateStr);
        }

        LocalDateDoubleTimeSeriesBuilder builder = builders.computeIfAbsent(id, k -> LocalDateDoubleTimeSeries.builder());
        builder.put(date, value);
      }
      return MapStream.of(builders).mapValues(builder -> builder.build()).toMap();
    } catch (RuntimeException ex) {
      throw new ParseFailureException(
          ex,
          "Error parsing CSV file '{fileName}': {exceptionMessage}",
          CharSources.extractFileName(resource),
          ex.getMessage());
    }
  }

  //-------------------------------------------------------------------------
  /**
   * Restricted constructor.
   */
  private FixingSeriesCsvLoader() {
  }

}
