import kdu_jni.Kdu_tile;
import kdu_jni.Kdu_resolution;
import kdu_jni.Kdu_tile_comp;
import kdu_jni.Kdu_dims;
import kdu_jni.Kdu_coords;
import kdu_jni.Kdu_coords;

import java.io.IOException;

import java.net.URL;

import java.awt.Point;

import java.util.LinkedList;
import java.util.ArrayList;
import java.util.ListIterator;


public class JP2Reader extends JP2Cache //implements Runnable
{
  private JP2Image image;
  private Thread myThread;
  private boolean finish;
  private HTTPReader client;
  private ArrayList packetsList[];
  private int packetsToRead;
  private int packetsOffsets[];
  private int offset[][][][][];
  private int length[][][][][];
  private byte pltBuffer[][];
  private ArrayList tilePartList;
  private ArrayList pltReference[];
  private JP2ImageView actualView;

  private static final int SOC_MARKER = 0;
  private static final int SOT_MARKER = 1;
  private static final int SOD_MARKER = 2;
  private static final int EOC_MARKER = 3;
  private static final int TLM_MARKER = 11;
  private static final int PLT_MARKER = 13;

  private static final int markers[]  = {0xFF4F, 0xFF90, 0xFF93, 0xFFD9, 0xFF51,
                                         0xFF52, 0xFF53, 0xFF5C, 0xFF5D, 0xFF5E,
                                         0xFF5F, 0xFF55, 0xFF57, 0xFF58, 0xFF60,
                                         0xFF61, 0xFF91, 0xFF92, 0xFF63, 0xFF64};

  public void addTLMSegment(byte data[])
  {
    int tile;
    int length;
    int dataPos;
    int indexLen = ((data[1] >> 4) & 3);
    int lengthLen = (2 << (data[1] >> 6));
    int numTileParts = (data.length - 2) / (indexLen + lengthLen);

    dataPos = 2;
    for(int i = 0; i < numTileParts; i++) {
      tile = 0;
      length = 0;

      for(int j = 0; j < indexLen; j++) tile = ((tile << 8) | ((int)data[dataPos++] & 0x000000ff));
      for(int j = 0; j < lengthLen; j++) length = ((length << 8) | ((int)data[dataPos++] & 0x000000ff));

      tilePartList.add(new Point(tile, length));
    }
  }

  public JP2Reader(JP2Image jp2Image) throws Exception
  {
    int marker;
    byte data[];
    int markerID;
    int markerLen;
    int tilePartOffset;

    myThread = null;
    pltBuffer = null;
    image = jp2Image;

    tilePartList = new ArrayList();

    client = new HTTPReader();
    client.open(new URL(image.getImageName()));

    data = client.read(2);
    marker = getDataShort(data, 0);

    if(marker != markers[SOC_MARKER])
      throw new JP2Exception("El contenido de la imagen debe comenzar por el marcador SOC.");

    addToMainHeader(data);

    data = new byte[4];
    tilePartOffset = 0;

    do {
      client.read(data);

      marker = getDataShort(data, 0);
      markerLen = getDataShort(data, 2) - 2;

      //System.out.println("Leo el marcador " + Integer.toHexString(marker));

      markerID = -1;
      for(int m = 0; m < markers.length; m++)
        if(marker == markers[m]) { markerID = m; break; }

      if(markerID == -1) {
        throw new JP2Exception("Se ha encontrado un marcador invalido en el codestream.");

      } else if(markerID == SOT_MARKER) {
        tilePartOffset = client.getFilePos() - 4;

      } else if(markerID == TLM_MARKER) {
        addTLMSegment(client.read(markerLen));

      } else {
        addToMainHeader(data);
        addToMainHeader(client.read(markerLen));
      }
    } while(markerID != SOT_MARKER);

    completeMainHeader();

    int numTileParts = tilePartList.size();

    if(numTileParts == 0)
      throw new JP2Exception("El archivo de imagen no contiene el marcador TLM.");

    packetsOffsets = new int[numTileParts];

    pltReference = new ArrayList[numTileParts];
    for(int i = 0; i < numTileParts; i++)
      pltReference[i] = new ArrayList();

    client.setFilePos(tilePartOffset);

    for(int t = 0; t < numTileParts; t++) {
      do {
        client.read(data);

        marker = getDataShort(data, 0);
        markerLen = getDataShort(data, 2) - 2;

        //System.out.println("Tile: Leo el marcador " + Integer.toHexString(marker));

        markerID = -1;
        for(int m = 0; m < markers.length; m++)
          if(marker == markers[m]) { markerID = m; break; }

        if(markerID == -1) {
          throw new JP2Exception("Se ha encontrado un marcador invalido en el codestream.");

        } else if(markerID == SOD_MARKER) {
          packetsOffsets[t] = client.getFilePos() - 2;

        } else if(markerID == PLT_MARKER) {
          pltReference[t].add(new Point(client.getFilePos() + 1, markerLen - 1));
          client.skip(markerLen);

        } else {
          addToTileHeader(data);
          addToTileHeader(client.read(markerLen));
        }
      } while(markerID != SOD_MARKER);

      tilePartOffset += ((Point)tilePartList.get(t)).y;
      client.setFilePos(tilePartOffset);
    }

    completeTileHeader();

    for(int i = 0; i < numTileParts; i++)
      if(pltReference[i].size() == 0)
        throw new JP2Exception("Es necesario que la imagen contenga marcadores PLT.");

    setInitialScope();
    client.close();
  }

  private int getDataShort(byte d[], int off)
  {
    return (((int)d[off + 0] << 8) & 0x0000ff00) | ((int)d[off + 1] & 0x000000ff);
  }

  int resMax;
  int numLayers;
  int numComponents;
  int numPrecsX[];
  int numPrecsY[];

  private void createIndex(int r) throws Exception
  {
    byte pbyte;
    int pltPos;
    int packetOffset;
    int packetLength;

    /**/System.out.println(">> Construyo el indice de la resolucion " + r);

    offset[r] = new int[numLayers][][][];
    length[r] = new int[numLayers][][][];

    pltPos = 0;
    packetOffset = packetsOffsets[r];

    for(int i = 0; i < pltReference[r].size(); i++) {
      Point range = (Point)(pltReference[r].get(i));
      client.addRange(range.x, range.y);
    }
    client.request();

    for(int l = 0; l < numLayers; l++) {
      offset[r][l] = new int[numComponents][][];
      length[r][l] = new int[numComponents][][];

      for(int c = 0; c < numComponents; c++) {
        offset[r][l][c] = new int[numPrecsY[r]][];
        length[r][l][c] = new int[numPrecsY[r]][];

        for(int py = 0; py < numPrecsY[r]; py++) {
          offset[r][l][c][py] = new int[numPrecsX[r]];
          length[r][l][c][py] = new int[numPrecsX[r]];

          for(int px = 0; px < numPrecsX[r]; px++) {

            packetLength = 0;
            for(;;) {
              pbyte = (byte)client.read();
              packetLength |= (pbyte & 127);
              if((pbyte & 128) == 0) break;
              packetLength <<= 7;
            }

            offset[r][l][c][py][px] = packetOffset;
            length[r][l][c][py][px] = packetLength;

            packetOffset += packetLength;
          }
        }
      }
    }
  }

  public void init() throws Exception
  {
    Kdu_tile tile;
    Kdu_resolution res;
    Kdu_tile_comp tile_comp;
    Kdu_dims precs = new Kdu_dims();

    tile = image.getCodestream().Open_tile(new Kdu_coords(0, 0));
    tile_comp = tile.Access_component(0);

    numLayers = tile.Get_num_layers();
    numComponents = tile.Get_num_components();
    resMax = tile_comp.Get_num_resolutions();

    numPrecsX = new int[resMax];
    numPrecsY = new int[resMax];

    for(int r = 0; r < resMax; r++) {
      res = tile_comp.Access_resolution(r);
      res.Get_valid_precincts(precs);
      numPrecsX[r] = precs.Access_size().Get_x();
      numPrecsY[r] = precs.Access_size().Get_y();
    }

    tile.Close();

    offset = new int[resMax][][][][];
    length = new int[resMax][][][][];

    for(int r = 0; r < resMax; r++)
      offset[r] = length[r] = null;
  }

 /* public void start()
  {
    if(myThread != null) return;

    finish = false;

    myThread = new Thread(this);
    myThread.setPriority(Thread.MIN_PRIORITY);
    myThread.start();
  }

  public void stop()
  {
    if(myThread == null) return;

    finish = true;

    try { myThread.join(); } catch(InterruptedException ex) { }
  }*/

 /* public boolean isStopped()
  {
    return (myThread == null);
  }*/

  private void prepareForReading() throws Exception
  {
    int precID;
    int numLayers;
    int numComponents;
    int numResolutions;
    Kdu_tile_comp comp;
    Kdu_resolution res;
    int initPrecX, initPrecY;
    int precsSizeX, precsSizeY;
    Kdu_dims precs = new Kdu_dims();

    System.out.println("Esperando a coger el lock...");
    image.lockCodestream();
    System.out.println("He codigo el lock");

    Kdu_tile tile = image.getCodestream().Open_tile(new Kdu_coords(0, 0));
    numComponents = tile.Get_num_components();
    numLayers = tile.Get_num_layers();

    packetsToRead = 0;
    packetsList = new ArrayList[numLayers];
    for(int i = 0; i < numLayers; i++) packetsList[i] = new ArrayList();

    for(int c = 0; (c < numComponents) && (!finish); c++) {
      comp = tile.Access_component(c);
      numResolutions = comp.Get_num_resolutions();

      for(int r = 0; (r < numResolutions) && (!finish); r++) {
        if(offset[r] == null) createIndex(r);

	res = comp.Access_resolution(r);
	res.Get_valid_precincts(precs);
        initPrecX = precs.Access_pos().Get_x();
        initPrecY = precs.Access_pos().Get_y();
        precsSizeX = precs.Access_size().Get_x();
        precsSizeY = precs.Access_size().Get_y();

	for(int j = 0; (j < precsSizeY) && (!finish); j++) {
	  for(int i = 0; i < (precsSizeX) && (!finish); i++) {

            precID = (int)res.Get_precinct_id(new Kdu_coords(initPrecX + i, initPrecY + j));
            int precLen = getPrecinctLength(precID);
            int packetLen;

            for(int l = 0; l < numLayers; l++) {
              packetLen = length[r][l][c][initPrecY + j][initPrecX + i];
              if(precLen >= packetLen) precLen -= packetLen;
              else {
                packetsList[l].add(
                   new PacketData(precID, offset[r][l][c][initPrecY + j][initPrecX + i], packetLen - precLen)
                );

                packetsToRead++;
                precLen = 0;
              }
            }
          }
	}
      }
    }

    tile.Close();
    image.unlockCodestream();
  }

  /*public void run()
  {
    try {
      client = new HTTPReader();
      client.open(new URL(image.getImageName()));
      client.setSequentialMode(false);

      prepareForReading();

      if(finish || (packetsToRead == 0)) {
        myThread = null;
        return;
      }
      int numPackets;
      byte data[] = null;
      PacketData packetData;

      int rangeOffset;
      int rangeLength;

      int layer = 0;
      int numLayers = packetsList.length;
      ListIterator iterator = packetsList[0].listIterator();

      LinkedList readList;
      ListIterator readIterator;

      while(layer < numLayers) {
        readList = new LinkedList();

        rangeOffset = 0;
        rangeLength = 0;

        numPackets = 0;
        client.clearRangeList();

        while((numPackets < 300) && (layer < numLayers)) {
          if(!iterator.hasNext()) {
            if(++layer < numLayers) iterator = packetsList[layer].listIterator();

          } else {
            packetData = (PacketData)iterator.next();
            readList.add(packetData);

            if(packetData.length > 1) {

              if(rangeOffset == 0) {
                rangeOffset = packetData.offset;
                rangeLength = packetData.length;
              } else {
                if(packetData.offset == (rangeOffset + rangeLength)) {
                  rangeLength += packetData.length;

                } else {
                  client.addRange(rangeOffset, rangeLength);
                  rangeOffset = packetData.offset;
                  rangeLength = packetData.length;
                  numPackets++;
                }
              }

            }
          }
        }

        if(rangeOffset != 0) {
          client.addRange(rangeOffset, rangeLength);
          numPackets++;
        }

        if(numPackets > 0) {
          client.request();

          readIterator = readList.listIterator();
          while(!finish && readIterator.hasNext()) {
            packetData = (PacketData)readIterator.next();
            if(packetData.length <= 1) addEmptyPacketToPrecinct(packetData.precID);
            else {
              data = client.read(packetData.length);
              addToPrecinct(data, packetData.precID);
            }
          }
        }
      }

      client.close();

      myThread = null;

    } catch(Exception ex) {

      myThread = null;

      System.out.println("Error en el sistema de comunicación.");
      ex.printStackTrace();
    }
  }*/

  private ReadThread readThread = new ReadThread();
  private WriteThread writeThread = new WriteThread();

  public void start()
  {
    if(!readThread.isStopped()) return;
    if(image.getActualView() == null) return;

    actualView = image.getActualView();

    try {
      client = new HTTPReader();
      client.open(new URL(image.getImageName()));
      client.setSequentialMode(false);

    } catch(Exception ex) {
      System.out.println("Error estableciendo la conexion con el servidor.");
      ex.printStackTrace();
      return;
    }

    finish = false;
    readThread.start();
  }

  public void stop()
  {
    finish = true;
    readThread.join();
    writeThread.join();
  }

  private class WriteThread implements Runnable
  {
    private int layer, layerPos;
    private boolean stopped = true;

    public void start()
    {
      start(0, 0);
    }

    public void start(int layer, int layerPos)
    {
      this.layer = layer;
      this.layerPos = layerPos;

      stopped = false;

      Thread myThread = new Thread(this);
      myThread.setPriority(Thread.MIN_PRIORITY);
      myThread.start();
    }

    public void run()
    {
      int numPackets;
      int rangeOffset;
      int rangeLength;

      PacketData packetData;
      int numLayers = packetsList.length;

      while((layer < numLayers) && (!finish)) {
        rangeOffset = 0;
        rangeLength = 0;

        numPackets = 0;
        client.clearRangeList();

        while((numPackets < 300) && (layer < numLayers) && (!finish)) {
          if(layerPos >= packetsList[layer].size()) {
            layerPos = 0;
            layer++;

          } else {
            packetData = (PacketData)(packetsList[layer].get(layerPos++));
            if(packetData.length > 1) {

              if(rangeOffset == 0) {
                rangeOffset = packetData.offset;
                rangeLength = packetData.length;
              } else {
                if(packetData.offset == (rangeOffset + rangeLength)) {
                  rangeLength += packetData.length;

                } else {
                  client.addRange(rangeOffset, rangeLength);
                  rangeOffset = packetData.offset;
                  rangeLength = packetData.length;
                  numPackets++;
                }
              }
            }
          }
        }

        if(rangeOffset != 0) {
          client.addRange(rangeOffset, rangeLength);
          numPackets++;
        }

        if((numPackets > 0) && (!finish)) {
          try {
            client.request();
            if(!client.isConnected()) break;

          } catch(Exception ex) {
            ex.printStackTrace();
            break;
          }
        }
      }

      stopped = true;
    }

    public void join()
    {
      while(!stopped) Thread.yield();
    }

    public boolean isStopped()
    {
      return stopped;
    }
  }

  private class ReadThread implements Runnable
  {
    private int layer, layerPos;
    private boolean stopped = true;

    public void start()
    {
      start(0, 0);
    }

    public void start(int layer, int layerPos)
    {
      this.layer = layer;
      this.layerPos = layerPos;

      stopped = false;

      Thread myThread = new Thread(this);
      myThread.setPriority(Thread.MIN_PRIORITY);
      myThread.start();
    }

    public void run()
    {
      System.out.println("Inicio hilo de lectura");

      try {
        System.out.println("Calculo los paquetes a leer");
        prepareForReading();

      } catch(Exception ex) {
        System.out.println("Error iniciando hilo de lectura.");
        ex.printStackTrace();
        stopped = true;
        return;
      }

      if(finish || (packetsToRead == 0)) {
        System.out.println("Finalizo hilo de lectura 2");
        if(!finish) actualView.setContentCompleted();
        stopped = true;
        return;
      }

      /**/System.out.println(">> Voy a leer " + packetsToRead + " paquetes " +  actualView.isContentCompleted());

      writeThread.start(layer, layerPos);

      byte data[] = null;
      PacketData packetData;

      int numLayers = packetsList.length;

      while((layer < numLayers) && (!finish)) {
        if(layerPos >= packetsList[layer].size()) {
          layerPos = 0;
          layer++;

        } else {
          packetData = (PacketData)(packetsList[layer].get(layerPos++));
          try {
            if(packetData.length <= 1) addEmptyPacketToPrecinct(packetData.precID);
            else {
              data = client.read(packetData.length);
              addToPrecinct(data, packetData.precID);

              if(!client.isConnected()) {
                System.out.println("Reseteo la conexion");
                writeThread.join();
                client.connect();
                writeThread.start(layer, layerPos);
              }
            }
          } catch(Exception ex) {
            ex.printStackTrace();
            stopped = true;
            return;
          }
        }
      }

      if(!finish) actualView.setContentCompleted();
      stopped = true;

      System.out.println("Finalizo hilo de lectura");
    }

    public void join()
    {
      System.out.println("Estoy esperando a que se acabe 1");
      while(!stopped) Thread.yield();
      System.out.println("He acabado 1");
    }

    public boolean isStopped()
    {
      return stopped;
    }
  }
}