import java.io.InputStream;
import java.io.OutputStream;
import java.io.IOException;
import java.io.EOFException;

import java.net.URL;
import java.net.Socket;


public class HTTPReader extends InputStream
{
  private Socket socket;              // Socket de conexion
  private InputStream inputStream;    // Flujo de entrada del socket
  private OutputStream outputStream;  // Flujo de salida del socket
  private boolean connected;          // Indica si el socket esta o no conectado
  private String host;                // Nombre del host
  private int port;                   // Puerto del servidor. Por defecto será el puerto 80
  private int bytesToRead;            // Numero de bytes a leer de la respuesta
  private String boundary;            // Es el delimitador final para respuestas divididas en partes
  private int numOfRanges;            // Numero de rangos incluidos en la lista
  private boolean closing;            // Indica si el servidor desea cerrar la conexion
  private int currentMessage;         // Indica el tipo de mensaje que actualmente se procesa
  private int fileOffset;             // Offset de lectura del archivo
  private int seqBufferSize;          // Tamaño del bloque a leer en el modo secuencial
  private boolean sequentialMode;     // Indica si estamos trabajando en modo secuencial o no
  private StrBuffer header;           // Buffer para albergar la cabecera
  private StrBuffer dataLine;         // Buffer para albergar la ultima linea leida
  private StrBuffer rangeList;        // Buffer para albergar la lista de rangos

  private static final byte CRLF[] = {13, 10};          // Retorno de carro y salto de linea

  private static final int HTTP_NO_MESSAGE        = 0;  // No existe ningun mensaje actualmente
  private static final int HTTP_NORMAL_MESSAGE    = 1;  // El mensaje actual no es de tipo 'multipart'
  private static final int HTTP_MULTIPART_MESSAGE = 2;  // El mensaje actual es de tipo 'multipart'

  private static int totalBytesRead = 0;                // Numero total de bytes leidos


  public HTTPReader()
  {
    connected = false;
    seqBufferSize = 200;
    sequentialMode = true;

    header = new StrBuffer();
    dataLine = new StrBuffer();
    rangeList = new StrBuffer();
  }

  public static int getTotalBytesRead()
  {
    return totalBytesRead;
  }

  public static void resetTotalBytesRead()
  {
    totalBytesRead = 0;
  }

  public boolean isConnected()
  {
    return connected;
  }

  public void setSequentialMode(boolean value)
  {
    if(sequentialMode != value) {
      try {
        while(bytesToRead > 0) read();
      } catch(IOException ex) { }

      if(value) fileOffset = 0;
    }

    sequentialMode = value;
  }

  public boolean isSequentialMode()
  {
    return sequentialMode;
  }

  public void setSequentialBlockSize(int size)
  {
    seqBufferSize = size;
  }

  public synchronized void open(URL url) throws IOException
  {
    if(!connected) {
      port = 80;
      if(url.getPort() != -1) port = url.getPort();
      host = url.getHost();

      header.copy("GET ").append(url.getPath()).append(" HTTP/1.1").append(CRLF);
      header.append("Host: ").append(host).append(CRLF);
      header.append("Connection: Keep-Alive").append(CRLF);

      clearRangeList();

      fileOffset = 0;
      currentMessage = HTTP_NO_MESSAGE;

      connect();
    }
  }

  public synchronized void connect() throws IOException
  {
    if(!connected) {
      try {
        socket = new Socket(host, port);
        outputStream = socket.getOutputStream();
        inputStream = socket.getInputStream();

      } catch(IOException ex) {
        throw new HTTPException("No se ha podido establecer la conexion con el servidor HTTP.");
      }

      closing = false;
      connected = true;
    }
  }

  public void close() throws IOException
  {
    if(connected) {
      connected = false;
      socket.close();
    }
  }

  private String readLine() throws IOException
  {
    byte car;
    dataLine.setLength(0);

    for(;;) {
      car = (byte)inputStream.read();
      if(car == -1) throw new EOFException();
      else if(car == CRLF[0]) break;
      else dataLine.append(car);
    }

    //System.out.println(dataLine.toString());

    if(inputStream.read() != CRLF[1]) throw new EOFException();
    totalBytesRead += (dataLine.getLength() + 2);
    return dataLine.toString();
  }

  private void ignoreLines(int numLines) throws IOException
  {
    byte car;

    for(int i = 0; i < numLines; i++) {
      do {
        car = (byte)inputStream.read();
        if(car == -1) throw new EOFException();
        totalBytesRead++;
      } while(car != CRLF[0]);

      if(inputStream.read() != CRLF[1]) throw new EOFException();
      totalBytesRead++;
    }
  }

  public synchronized void addRange(int offset, int length)
  {
    if(numOfRanges > 0) rangeList.append(",");
    else rangeList.copy("Range: bytes=");
    rangeList.append(offset).append("-").append(offset + length - 1);
    numOfRanges++;
  }

  public synchronized void clearRangeList()
  {
    numOfRanges = 0;
  }

  private void checkConnection() throws IOException
  {
    if(!connected) {
      if(!sequentialMode) throw new HTTPException("La conexion con el servidor HTTP está cerrada.");
      else connect();
    }
  }

  public synchronized void request() throws IOException
  {
    checkConnection();

    outputStream.write(header.getBuffer(), 0, header.getLength());

    if(numOfRanges > 0) {
      outputStream.write(rangeList.getBuffer(), 0, rangeList.getLength());
      outputStream.write(CRLF);
    }

    outputStream.write(CRLF);

    clearRangeList();
  }

  private void readHeader() throws IOException
  {
    if(currentMessage == HTTP_NO_MESSAGE) {
      checkConnection();

      if(sequentialMode) {
        addRange(fileOffset, seqBufferSize);
        request();
      }

      int pos2Dots, lastStatusCode;
      String line, serverProtocol, lastResponse, header, headerVal;

      line = readLine();
      serverProtocol = line.substring(0, 8);
      lastStatusCode = Integer.parseInt(line.substring(9, 12));
      lastResponse = line.substring(13);

      if(!serverProtocol.equals("HTTP/1.1"))
        throw new HTTPException("El servidor no acepta el protocolo HTTP/1.1.");

      if(lastStatusCode >= 300)
        throw new HTTPException("Peticion HTTP denegada por el servidor (" + lastStatusCode + " " + lastResponse + ").");

      currentMessage = HTTP_NORMAL_MESSAGE;

      line = readLine();
      while(line.length() != 0) {
        pos2Dots = line.indexOf(":");
        header = line.substring(0, pos2Dots);
        headerVal = line.substring(pos2Dots + 2);

        if(header.compareToIgnoreCase("Content-Type") == 0) {
          if(headerVal.startsWith("multipart/byteranges")) {
            boundary = "--" + headerVal.substring(31) + "--";
            currentMessage = HTTP_MULTIPART_MESSAGE;
          }

        } else if(header.compareToIgnoreCase("Content-Length") == 0) {
          bytesToRead = Integer.parseInt(headerVal);

        } else if(header.compareToIgnoreCase("Connection") == 0) {
          if(headerVal.compareToIgnoreCase("close") == 0) closing = true;
        }

        line = readLine();
      }

      if(currentMessage == HTTP_MULTIPART_MESSAGE) ignoreLines(2);
    }

    if(currentMessage == HTTP_NORMAL_MESSAGE) {
    }

    if(currentMessage == HTTP_MULTIPART_MESSAGE) {
      String line = readLine();
      while(line.length() != 0) {
        if(line.toUpperCase().startsWith("CONTENT-RANGE")) {
          int offHyphen = line.indexOf("-", 21);
          int offSlash = line.indexOf("/", 21);
          int ia = Integer.parseInt(line.substring(21, offHyphen));
          int ib = Integer.parseInt(line.substring(offHyphen + 1, offSlash));
          bytesToRead = ib - ia + 1;
        }
        line = readLine();
      }
    }
  }

  private void readFooter() throws IOException
  {
    if(currentMessage == HTTP_NO_MESSAGE) {
    }

    if(currentMessage == HTTP_NORMAL_MESSAGE) {
      currentMessage = HTTP_NO_MESSAGE;
      if(closing) close();
    }

    if(currentMessage == HTTP_MULTIPART_MESSAGE) {
      ignoreLines(1);

      String line = readLine();

      if(line.equals(boundary)) {
        currentMessage = HTTP_NO_MESSAGE;
        if(closing) {
          close();
          System.out.println("Cierro la conexion!!");
        }
      }
    }
  }

  /*long initTime = 0;
  int countBytes = 0;*/

  public int read() throws IOException
  {
    if(bytesToRead == 0) readHeader();

  /*  if(initTime == 0) initTime = System.currentTimeMillis();
    else {
      countBytes++;
      if(countBytes > 4024) {
        countBytes = 0;
        long timeres = 1000 - (System.currentTimeMillis() - initTime);
        if(timeres > 0) {
          try { Thread.sleep(timeres); } catch(InterruptedException ex) { }
        }
        initTime = System.currentTimeMillis();
      }
    }*/

    int c = inputStream.read();

    if(c == -1) throw new EOFException();
    else {
      fileOffset++;
      bytesToRead--;
      totalBytesRead++;
    }

    if(bytesToRead == 0) readFooter();

    return c;
  }

  public byte[] read(int length) throws IOException
  {
    byte[] result = new byte[length];
    read(result);
    return result;
  }

  public long skip(long n) throws IOException
  {
    long skipped = n;

    while((bytesToRead > 0) && (n > 0)) {
      totalBytesRead--;
      read();
      n--;
    }

    fileOffset += n;

    return skipped;
  }

  public int getFilePos()
  {
    return fileOffset;
  }

  public void setFilePos(int pos) throws IOException
  {
    if(pos >= fileOffset) skip(pos - fileOffset);
    else {
      while(bytesToRead > 0) {
        totalBytesRead--;
        read();
      }

      fileOffset = pos;
    }
  }

  public int available() throws IOException
  {
    return bytesToRead;
  }
}