/*
 * Copyright (c) 2009, Takeyuki Nagao
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or
 * without modification, are permitted provided that the
 * following conditions are met:
 * 
 *  * Redistributions of source code must retain the above
 *    copyright notice, this list of conditions and the
 *    following disclaimer.
 *  * Redistributions in binary form must reproduce the above
 *    copyright notice, this list of conditions and the
 *    following disclaimer in the documentation and/or other
 *    materials provided with the distribution.
 *    
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
 * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
 * OF SUCH DAMAGE.
 */

package dvi.util.csv;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import dvi.util.DviUtils;
public class CsvData<K, V> {
  private static final Logger LOGGER = Logger.getLogger(CsvData.class
      .getName());
  private static final String DEFAULT_QUOTE_CHAR = "\"";
  private static final String DEFAULT_SEPARATOR = ",";
  
  private final List<Map<K, V>> lines = new ArrayList<Map<K, V>>();
  private final List<K> defaultKeys = new ArrayList<K>();
  private final CsvCellCodec<K, V> codec;
  
  public CsvData(CsvCellCodec<K, V> codec)
  {
    if (codec == null)
      throw new IllegalArgumentException("CSV cell codec can't be null");
    this.codec = codec;
  }
  
  private Map<K, V> line;
  
  public void beginLine()
  {
    if (line != null) {
      throw new IllegalStateException("line is not null: last line is not closed.");
    }
    
    line = newLine();
  }

  protected Map<K, V> newLine() {
    return new HashMap<K, V>();
  }
  
  public void endLine()
  {
    lines.add(line);
    line = null;
  }
  
  public void put(K key, V value)
  {
    if (line == null) {
      throw new IllegalStateException("line is null: put() method is called before beginLine() is invoked.");
    }
    line.put(key, value);
    if (!defaultKeys.contains(key)) {
      defaultKeys.add(key);
    }
  }
  
  public V get(Object key)
  {
    if (line == null) {
      throw new IllegalStateException("line is null: get() method is called before beginLine() is invoked.");
    }
    return line.get(key);
  }
  
  public String [] getLines()
  {
    return encodeToCsv(null);
  }
  
  public int getRowCount() {
    return lines.size();
  }
  
  public int getColumnCount() {
    return defaultKeys.size();
  }
  
  public V get(int row, Object key) {
    Map<K, V> map = getRow(row);
    if (map == null) return null;
    return map.get(key);
  }
  
  public Map<K, V> getRow(int row) {
    if (row < 0 || getRowCount() <= row) {
      throw new IllegalArgumentException("Row index out of bounds: " + row);
    }
    
    Map<K, V> map = lines.get(row);
    return map;
  }


  public String [] encodeToCsv()
  {
    return encodeToCsv(null);
  }
  
  public String [] encodeToCsv(Collection<K> keys)
  {
    List<String> csvEncodedLines = new ArrayList<String>();
    
    if (keys == null) {
      keys = getDefaultKeys();
    }
    
    String q = getQuoteChar();
    String s = getSeparatorChar();

    {
      List<String> items = new ArrayList<String>();
      for (K key : keys) {
        String k = codec.encodeKey(key);
        k = escapeItem(k, q, s);
        items.add(k);
      }
      csvEncodedLines.add(DviUtils.join(s, items));
    }
    
    for (Map<K, V> csvLine : lines) {
      List<String> items = new ArrayList<String>();
      for (K key : keys) {
        V value = csvLine.get(key);
        String x = codec.encodeValue(key, value);
        x = escapeItem(x, q, s);
        items.add(x);
      }
      csvEncodedLines.add(DviUtils.join(s, items));
    }
    
    return csvEncodedLines.toArray(new String[csvEncodedLines.size()]);
  }
  
  protected String escapeItem(String x, final String quoteMark, final String separator)
  {
    final String q = quoteMark;
    
    if (x == null) {
      x = "";
    } else if (needEscape(x)) {
      x = q + x.replaceAll(q, q + q) + q;
    }
    return x;
  }
  
  private static final Pattern patNeedEscape = Pattern.compile("[\\r\\n,\"]");
  protected boolean needEscape(String x) {
    if (x == null) {
      return false;
    } else {
      Matcher mat = patNeedEscape.matcher(x);
      return mat.find();
    }
  }

  public void writeToFile(File file)
  throws IOException
  {
    LOGGER.fine("Writing CSV data to file: " + file);
    if (file == null) throw new IllegalArgumentException("file can't be null");
    FileOutputStream fos = new FileOutputStream(file);
    try {
      writeToStream(fos);
    } finally {
      fos.flush();
      DviUtils.silentClose(fos);
    }
  }
  
  public void writeToStream(OutputStream os)
  throws IOException
  {
    if (os == null) throw new IllegalArgumentException("output stream can't be null");
    try {
      PrintWriter pw = new PrintWriter(os);
      for (String line : getLines()) {
        pw.println(line);
      }
      pw.flush();
      pw.close();
    } finally {
      os.flush();
      DviUtils.silentClose(os);
    }
  }
  
  public void readFromFile(File file) throws IOException, CsvException
  {
    LOGGER.fine("Reading CSV data from file: " + file);
    if (file == null) throw new IllegalArgumentException("file can't be null");
    FileInputStream fis = new FileInputStream(file);
    try {
      readFromStream(fis);
    } finally {
      DviUtils.silentClose(fis);
    }
  }
  
  public void readFromStream(InputStream is) throws IOException, CsvException
  {
    CsvParser<K, V> parser = new CsvParser<K, V>(codec, this);
  
    String [] lines = DviUtils.readLinesFromStream(is);
    for (String line : lines) {
      LOGGER.fine("sending line to parser: " + line);
      parser.feed(line);
    }
    parser.close();
  }
  
  private String getSeparatorChar() {
    return DEFAULT_SEPARATOR;
  }

  private String getQuoteChar() {
    return DEFAULT_QUOTE_CHAR;
  }

  protected Collection<K> getDefaultKeys() {
    return Collections.unmodifiableList(defaultKeys);
  }

  public void putAll(Map<K, V> map) {
    for (Map.Entry<K, V> entry : map.entrySet()) {
      put(entry.getKey(), entry.getValue());
    }
  }
  
  public int hashCode()
  {
    int hashCode = 0;
    
    final int rows = getRowCount();
    for (int i=0; i<rows; i++) {
      Map<?, ?> map = getRow(i);
      hashCode = map.hashCode() + 33 * hashCode;
    }
    return hashCode;
  }
  
  public boolean equals(Object o) {
    if (!(o instanceof CsvData<?, ?>)) {
      return false;
    }
    CsvData<?, ?> cd = (CsvData<?, ?>) o;
    final int rows = getRowCount();
    final int cols = getColumnCount();
    if (cd.getColumnCount() != cols) return false;
    if (cd.getRowCount() != rows) return false;
    for (int i=0; i<rows; i++) {
      Map<?, ?> map1 = cd.getRow(i);
      Map<?, ?> map2 = getRow(i);
      if (map1.equals(map2)) {
        return false;
      }
    }
    return true;
  }
}
