/**
 * Copyright (c) 2002-2012 IBM Corporation and others.
 * All rights reserved.   This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v2.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v20.html
 * 
 * Contributors: 
 *   IBM - Initial API and implementation
 */
package org.eclipse.emf.ecore.xmi.impl;


import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.Iterator;
import java.util.ListIterator;

import org.eclipse.emf.common.util.BasicEList;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.xmi.XMIPlugin;


/**
 * A String Buffer that never reallocates
 */
public class StringSegment extends BasicEList<StringSegment.Element>
{
  private static final long serialVersionUID = 1L;

  private static final String LINE_SEPARATOR = System.getProperty("line.separator");

  protected final static int LIST_SIZE = 100;

  protected static final int ELEMENT_SIZE = 1000;

  protected static final int BUFFER_SIZE = 8192;

  protected int segmentCapacity;

  protected byte[] outputbytes;

  protected char[] outputchars;

  protected char[] buffer;

  protected Element cursor;

  protected int cursorIndex = 0;

  protected String lineSeparator = LINE_SEPARATOR;

  protected String temporaryFileName;

  protected Writer temporaryFile;

  protected int bufferPosition;

  protected String firstString;

  public StringSegment()
  {
    this(LIST_SIZE);
  }

  public StringSegment(int minimumCapacity)
  {
    this(minimumCapacity, ELEMENT_SIZE);
  }

  public StringSegment(int minimumCapacity, int segmentCapacity)
  {
    super(minimumCapacity);
    add(cursor = new Element(this.segmentCapacity = segmentCapacity));
    outputchars = new char [BUFFER_SIZE];
  }

  public StringSegment(String temporaryFileName)
  {
    this(LIST_SIZE, ELEMENT_SIZE);
    setTemporaryFileName(temporaryFileName);
  }

  public void setTemporaryFileName(String tempFile)
  {
    temporaryFileName = tempFile;
    if (temporaryFileName != null)
    {
      buffer = new char [BUFFER_SIZE];
    }
    else
    {
      buffer = null;
    }
  }

  /**
   * @since 2.9
   */
  public void setLineSeparator(String lineSeparator)
  {
    this.lineSeparator = lineSeparator == null || lineSeparator.equals(Resource.OPTION_LINE_DELIMITER_UNSPECIFIED) ? LINE_SEPARATOR : lineSeparator;
  }

  public String getTemporaryFileName()
  {
    return temporaryFileName;
  }

  @Override
  protected Object[] newData(int capacity)
  {
    return new Element [capacity];
  }

  public void reset()
  {
    bufferPosition = 0;
    cursor = (Element)data[0];
    cursorIndex = 0;
    for (int i = 0; i < size; i++)
    {
      ((Element)data[i]).size = 0;
    }
  }

  public void add(String newString)
  {
    // System.err.println("add = ["+newString+"]");

    // If there is a temporary file...
    //
    if (temporaryFile != null)
    {
      if (firstString == null)
      {
        firstString = newString;
      }
      int length = newString.length();
      if (length + bufferPosition >= buffer.length)
      {
        try
        {
          temporaryFile.write(buffer, 0, bufferPosition);
        }
        catch (IOException exception)
        {
          XMIPlugin.INSTANCE.log(exception);
        }
        bufferPosition = 0;
        if (length > buffer.length)
        {
          buffer = new char [length];
        }
      }
      newString.getChars(0, length, buffer, bufferPosition);
      bufferPosition += length;
      return;
    }

    // This is the cheapest and most common case.
    //
    if (cursor.size < segmentCapacity)
    {
      cursor.add(newString);
      return;
    }

    Element oldCursor = cursor;
    int index = size - 1;
    if (cursorIndex < index)
    {
      cursor = (Element)data[++cursorIndex];
      if (cursor.size == 0)
      {
        cursor.add(newString);
        return;
      }
    }

    cursor = new Element(segmentCapacity);
    cursor.add(newString);

    // The first case is the most common case.
    // It is slightly cheaper to call add without an index since an index will be range checked.
    //  
    if (data[index] == oldCursor)
    {
      super.add(cursor);
      cursorIndex = ++index;
    }
    else
    {
      // This case can only happen if we are reset to a mark and we've got lots of XMLNS attributes to write.
      //
      int counter = 0;
      while (counter < index)
      {
        if (data[counter++] == oldCursor)
        {
          cursorIndex = counter;
          super.add(cursorIndex, cursor);
          break;
        }
      }
    }
  }

  public void addLine()
  {
    add(lineSeparator);
  }

  public Object mark()
  {
    Element result = cursor;
    if (cursor.size == 0)
    {
      result.add("");
    }
    int i = size - 1;
    if (cursorIndex < i)
    {
      cursor = (Element)data[++cursorIndex];
    }
    else
    {
      cursorIndex++;
      cursor = new Element(segmentCapacity);
      super.add(cursor);
    }
    return result;
  }

  public void startFileBuffering()
  {
    if (temporaryFileName != null && temporaryFile == null)
    {
      try
      {
        temporaryFile = new OutputStreamWriter(new FileOutputStream(temporaryFileName), "UTF8");
      }
      catch (IOException exception)
      {
        // If we can't create one, too bad.
      }
    }
  }

  public void resetToMark(Object mark)
  {
    if (temporaryFile != null)
    {
      cursor.add("");
      try
      {
        temporaryFile.write(buffer, 0, bufferPosition);
        temporaryFile.close();
      }
      catch (IOException exception)
      {
        XMIPlugin.INSTANCE.log(exception);
      }
      temporaryFile = null;
    }
    cursor = (Element)mark;
    for (int i = 0; i < data.length; i++)
    {
      if (data[i] == cursor)
      {
        cursorIndex = i;
        return;
      }
    }
  }

  public int getLength()
  {
    Element[] elements = (Element[])data;
    int length = 0;
    for (int i = 0; i < size; ++i)
    {
      Element element = elements[i];
      int segmentSize = element.size;
      for (int j = 0; j < segmentSize; ++j)
      {
        String s = element.data[j];
        length += s.length();
      }
    }
    return length;
  }

  public int getChars(char[] destination, int position)
  {
    Element[] elements = (Element[])data;
    for (int i = 0; i < size; ++i)
    {
      Element element = elements[i];
      int segmentSize = element.size;
      for (int j = 0; j < segmentSize; ++j)
      {
        String string = element.data[j];
        int length = string.length();
        string.getChars(0, length, destination, position);
        position += length;
      }
    }
    return position;
  }

  public void writeAscii(OutputStream os, int flushThreshold) throws IOException
  {
    // Ensure the outputbytes are allocated and that it and outputchars have the same length
    //
    int requiredLength = Math.max( BUFFER_SIZE, outputchars.length );
    if( outputbytes == null || outputbytes.length < requiredLength )
    {
      outputbytes = new byte [requiredLength];
    }

    Element[] elements = (Element[])data;
    int position = 0;
    int count = 0;

    for (int i = 0; i < size; ++i)
    {
      Element element = elements[i];
      int segmentSize = element.size;
      for (int j = 0; j < segmentSize; ++j)
      {
        String string = element.data[j];
        int length = string.length();
        if (length + position >= outputchars.length)
        {
          for (int x = 0; x < position; x++)
          {
            outputbytes[x] = (byte)(outputchars[x] & 0xFF);
          }
          os.write(outputbytes, 0, position);
          position = 0;
          if (length > outputchars.length)
          {
            outputchars = new char [length];
            outputbytes = new byte [length];
          }
        }
        string.getChars(0, length, outputchars, position);
        position += length;
        count += length;
        if (count > flushThreshold)
        {
          os.flush();
          count = 0;
        }
      }
    }
    for (int x = 0; x < position; x++)
    {
      outputbytes[x] = (byte)(outputchars[x] & 0xFF);
    }

    os.write(outputbytes, 0, position);

    String temporaryFileName = this.temporaryFileName;
    if (temporaryFileName != null)
    {
      InputStream inputStream = new FileInputStream(temporaryFileName);
      for (int length = inputStream.read(outputbytes, 0, outputbytes.length); length > 0; length = inputStream.read(
        outputbytes,
        0,
        outputbytes.length))
      {
        os.write(outputbytes, 0, length);
        count += length;
        if (count > flushThreshold)
        {
          os.flush();
          count = 0;
        }
      }
      inputStream.close();
      new File(temporaryFileName).delete();
    }
  }
  
  /**
   * @deprecated since 2.2 - Instead use #write(Writer, int)
   * @param os
   * @param flushThreshold
   * @throws IOException
   */
  @Deprecated
  public void write(OutputStreamWriter os, int flushThreshold) throws IOException
  {
    write((Writer)os, flushThreshold);
  }

  public void write(Writer os, int flushThreshold) throws IOException
  {
    Element[] elements = (Element[])data;
    int position = 0;
    int count = 0;
    for (int i = 0; i < size; ++i)
    {
      Element element = elements[i];
      int segmentSize = element.size;
      for (int j = 0; j < segmentSize; ++j)
      {
        String string = element.data[j];
        int length = string.length();
        if (length + position >= outputchars.length)
        {
          os.write(outputchars, 0, position);
          position = 0;
          if (length > outputchars.length)
          {
            outputchars = new char [length];
          }
        }
        string.getChars(0, length, outputchars, position);
        position += length;
        count += length;
        if (count > flushThreshold)
        {
          os.flush();
          count = 0;
        }
      }
    }
    os.write(outputchars, 0, position);

    String temporaryFileName = this.temporaryFileName;
    if (temporaryFileName != null)
    {
      InputStreamReader reader = new InputStreamReader(new FileInputStream(temporaryFileName), "UTF8");
      for (int length = reader.read(outputchars, 0, outputchars.length); length > 0; length = reader.read(
        outputchars,
        0,
        outputchars.length))
      {
        os.write(outputchars, 0, length);
        count += length;
        if (count > flushThreshold)
        {
          os.flush();
          count = 0;
        }
      }
      reader.close();
      new File(temporaryFileName).delete();
    }

  }

  protected static class Element
  {
    int size;

    String[] data;

    Element(int capacity)
    {
      data = new String [capacity];
    }

    void add(String newString)
    {
      data[size++] = newString;
    }
  }

  @SuppressWarnings("unchecked")
  @Override
  public Iterator<Element> iterator()
  {
    // TODO This is really quite attrocious since there is code that will assume an iterator that returns strings!
    return (ListIterator<Element>)(ListIterator<?>)new SegmentIterator();
  }

  @SuppressWarnings("unchecked")
  @Override
  public ListIterator<Element> listIterator()
  {
    // TODO This is really quite attrocious since there is code that will assume an iterator that returns strings!
    return (ListIterator<Element>)(Iterator<?>)new SegmentIterator();
  }

  public Iterator<String> stringIterator()
  {
    return new SegmentIterator();
  }

  protected class SegmentIterator implements ListIterator<String>
  {
    protected int outerIndex = 0;

    protected int innerIndex = 0;

    SegmentIterator()
    {
      super();
    }

    public boolean hasNext()
    {
      return outerIndex < size - 1 || (outerIndex == size - 1 && innerIndex < ((Element)data[outerIndex]).size);
    }

    public boolean hasPrevious()
    {
      return outerIndex > 0 || innerIndex > 0;
    }

    public String next()
    {
      Element element = (Element)data[outerIndex];
      if (innerIndex < element.size)
      {
        return element.data[innerIndex++];
      }
      else
      {
        innerIndex = 1;
        return ((Element)data[++outerIndex]).data[0];
      }
    }

    public String previous()
    {
      if (innerIndex > 0)
      {
        return ((Element)data[outerIndex]).data[--innerIndex];
      }
      else
      {
        Element element = (Element)data[--outerIndex];
        innerIndex = element.size - 1;
        return element.data[innerIndex];
      }
    }

    public void add(String newElement)
    {
      throw new UnsupportedOperationException(SegmentIterator.class.toString());
    }

    public void remove()
    {
      throw new UnsupportedOperationException(SegmentIterator.class.toString());
    }

    public void set(String newElement)
    {
      throw new UnsupportedOperationException(SegmentIterator.class.toString());
    }

    public int nextIndex()
    {
      throw new UnsupportedOperationException(SegmentIterator.class.toString());
    }

    public int previousIndex()
    {
      throw new UnsupportedOperationException(SegmentIterator.class.toString());
    }
  }
}
