/*
 * Licensed Materials - Property of Perforce Software, Inc. 
 * © Copyright Perforce Software, Inc. 2014, 2021 
 * © Copyright IBM Corp. 2009, 2014
 * © Copyright ILOG 1996, 2009
 * All Rights Reserved.
 *
 * Note to U.S. Government Users Restricted Rights:
 * The Software and Documentation were developed at private expense and
 * are "Commercial Items" as that term is defined at 48 CFR 2.101,
 * consisting of "Commercial Computer Software" and
 * "Commercial Computer Software Documentation", as such terms are
 * used in 48 CFR 12.212 or 48 CFR 227.7202-1 through 227.7202-4,
 * as applicable.
 */
package lod;

import java.io.DataInputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.LinkedList;

import ilog.views.chart.IlvDataInterval;
import ilog.views.chart.data.IlvDataPoints;
import ilog.views.chart.data.lod.IlvDataTile;
import ilog.views.chart.data.lod.IlvDataTileLoader;

/**
 * A data tile loader that reads data from a binary file.
 * <p>
 * The <code>DataTileLoader</code> class implements a threaded loading mechanism
 * that uses random file access to retrieve the data for a given tile.
 */
public class DataTileLoader implements IlvDataTileLoader {
  private IlvDataInterval xRange = new IlvDataInterval();
  private IlvDataInterval yRange = new IlvDataInterval();
  private LinkedList<IlvDataTile> tileQueue = new LinkedList<IlvDataTile>();
  private File file;
  private LoadThread loadThread;
  private long lag;

  /**
   * Initializes a new loader with the specified data file.
   * 
   * @param file
   *          The file from which data is read.
   */
  public DataTileLoader(File file) {
    parseDataFile(file);
    this.file = file;
    loadThread = new LoadThread();
    loadThread.start();
  }

  /**
   * Parses the data file to compute the limits of the x and y values.
   */
  private void parseDataFile(File file) {
    DataInputStream in = null;
    try {
      in = new DataInputStream(new FileInputStream(file));
      int len = in.available();
      int min = Integer.MAX_VALUE;
      int max = Integer.MIN_VALUE;
      byte[] bytes = new byte[len];
      in.read(bytes);
      int val;
      for (int i = 0; i < len; i += 4) {
        val = ((bytes[i] & 0xff) << 24) + ((bytes[i + 1] & 0xff) << 16) + ((bytes[i + 2] & 0xff) << 8)
            + ((bytes[i + 3] & 0xff));
        if (val > max)
          max = val;
        else if (val < min)
          min = val;
      }
      yRange.set(min, max);
      xRange.set(0, len / 4);
    } catch (Exception x) {
      x.printStackTrace();
    } finally {
      try {
        if (in != null)
          in.close();
      } catch (Exception x) {
        x.printStackTrace();
      }
    }
  }

  /**
   * The thread that handles data reading.
   */
  private class LoadThread extends Thread {
    RandomAccessFile rf = null;
    IlvDataTile tile;

    LoadThread() {
      super("TileLoadingThread");
    }

    /**
     * Cancels the loading of a tile.
     */
    void cancelLoading() {
      // The thread can be in a waiting state, so we interrupt it.
      // If it is not in such a state, the interrupt flag of the
      // thread will be set and we will be able to test it in the run method.
      interrupt();
      if (rf != null) {
        // The thread can be blocked in an IO request (for example: reading).
        // To interrupt it, we close the file, which should throw an
        // IOException in the run method.
        try {
          rf.close();
        } catch (Exception x) {
          x.printStackTrace();
        }
      }
    }

    /**
     * Waits for tile loading requests and executes them.
     */
    Override
    public void run() {
      while (true) {
        try {
          rf = null;
          tile = popTile();
          if (tile == null)
            continue;
          if (DataTileLoader.this.getLag() > 0)
            sleep(DataTileLoader.this.getLag());
          rf = new RandomAccessFile(DataTileLoader.this.file, "r");
          IlvDataInterval itv = tile.getRange();
          long start = (long) Math.floor(itv.getMin());
          if (start < 0)
            continue;
          rf.seek(start * 4);
          int count = (int) itv.getLength() + 1;
          IlvDataPoints dataPts = new IlvDataPoints(count);
          int byteCount = count * 4;
          byte[] data = new byte[byteCount];
          // For better performances, we read all the bytes at once
          // instead of using the readInt() method.
          rf.read(data);
          if (Thread.interrupted())
            continue;
          int val;
          for (int i = 0; i < byteCount; i += 4) {
            val = ((data[i] & 0xff) << 24) | ((data[i + 1] & 0xff) << 16) | ((data[i + 2] & 0xff) << 8)
                | ((data[i + 3] & 0xff));
            dataPts.add(start++, val);
          }
          if (Thread.interrupted())
            continue;
          tile.setData(dataPts);
          dataPts.dispose();
          tile.loadComplete();
          tile = null;
        } catch (EOFException e) {
          System.err.println("End of file reached");
        } catch (InterruptedException e) {
          // Can be because of cancelLoading.
          // e.printStackTrace();
        } catch (IOException e) {
          // Can be because of cancelLoading.
          // e.printStackTrace();
        } catch (Exception x) {
          x.printStackTrace();
        } finally {
          try {
            if (rf != null)
              rf.close();
          } catch (Exception x) {
            x.printStackTrace();
          }
        }
      }
    }
  }

  /**
   * Pops the first tile from the queue.
   */
  private synchronized IlvDataTile popTile() {
    try {
      while (tileQueue.size() == 0)
        wait(); // Put loading thread in waiting state.
    } catch (InterruptedException x) {
      return null;
    }
    return tileQueue.removeFirst();
  }

  /**
   * Puts a tile at the end of the queue.
   */
  private synchronized void pushTile(IlvDataTile tile) {
    tileQueue.addLast(tile);
    if (tileQueue.size() == 1)
      notify(); // Wake up loading thread.
  }

  /**
   * Loads the contents of the specified tile. This method appends the tile to
   * the tile queue.
   */
  Override
  public synchronized void load(IlvDataTile tile) throws Exception {
    pushTile(tile);
  }

  /**
   * Invoked by the tile controller when a tile is released.
   * <p>
   * This method removes the tile from the tile queue, or cancels the loading if
   * the tile is currently being processed.
   */
  Override
  public synchronized void release(IlvDataTile tile) {
    if (loadThread.tile == tile) {
      // The tile is currently processed by the loading thread.
      loadThread.cancelLoading();
    } else {
      // Just remove the tile from the queue.
      tileQueue.remove(tile);
    }
    tile.setData(null);
  }

  /**
   * Specifies a fictional duration during which the loading thread sleeps, thus
   * simulating a lag that can occur when dealing with network connections.
   */
  public void setLag(long lag) {
    this.lag = lag;
  }

  /**
   * Returns the lag.
   * 
   * @see #setLag
   */
  public final long getLag() {
    return lag;
  }

  /**
   * Returns the limits of the x values that will be provided by the loader.
   */
  Override
  public IlvDataInterval getXRange() {
    return new IlvDataInterval(xRange);
  }

  /**
   * Returns the limits of the y values that will be provided by the loader.
   */
  Override
  public IlvDataInterval getYRange() {
    return new IlvDataInterval(yRange);
  }
}