/*
    GNU LESSER GENERAL PUBLIC LICENSE
    Copyright (C) 2015 Uproot Labs India Pvt Ltd

    This library is free software; you can redistribute it and/or
    modify it under the terms of the GNU Lesser General Public
    License as published by the Free Software Foundation; either
    version 2.1 of the License, or (at your option) any later version.

    This library is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public
    License along with this library; if not, write to the Free Software
    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

 */

package org.lobobrowser.reuse;

import java.awt.Component;
import java.awt.Graphics;
import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutionException;

import javax.imageio.ImageIO;
import javax.swing.SwingUtilities;

import org.lobobrowser.LoboBrowser;
import org.cobraparser.util.gui.DefferedLayoutSupport;
import org.cobraparser.clientlet.ComponentContent;
import org.cobraparser.ua.NavigatorFrame;
import org.cobraparser.ua.NavigatorProgressEvent;
import org.cobraparser.ua.ProgressType;

class GrinderServer implements Runnable {
  private final NavigatorFrame frame;
  private final ServerSocket socket;

  public GrinderServer(final NavigatorFrame frame) throws IOException {
    this.frame = frame;
    socket = new ServerSocket(0);
    final Thread t = new Thread(this, "GrinderServer");
    t.setDaemon(true);
    t.start();

  }

  @Override
  public void run() {
    boolean done = false;
    while (!done) {
      try {
        final ServerSocket ss = socket;
        try (
          final Socket s = ss.accept()) {
          s.setSoTimeout(10000);
          s.setTcpNoDelay(true);
          try (
            final InputStream in = s.getInputStream()) {
            final Reader reader = new InputStreamReader(in);
            final BufferedReader br = new BufferedReader(reader);
            String commandLine = br.readLine();
            if (commandLine != null) {
              final int blankIdx = commandLine.indexOf(' ');
              final String command = blankIdx == -1 ? commandLine : commandLine.substring(0, blankIdx).trim();
              // System.out.println("Command: " + command);
              if ("TO".equals(command)) {
                if (blankIdx != -1) {
                  final String path = commandLine.substring(blankIdx + 1).trim();
                  handleTo(s, br, path);
                }
              } else if ("SET_SIZE".equals(command)) {
                if (blankIdx != -1) {
                  final String params = commandLine.substring(blankIdx + 1).trim();
                  handleResize(s, br, params);
                }
              } else if ("SCREENSHOT".equals(command)) {
                handleScreenShot(s, br);
              } else if ("CLOSE".equals(command)) {
                frame.closeWindow();
                done = true;
              } else if ("QUIT".equals(command)) {
                frame.closeWindow();
                LoboBrowser.shutdown();
                done = true;
              }
            }
          }
        }
      } catch (final Exception t) {
        t.printStackTrace(System.err);
      }
    }

  }

  private void handleResize(final Socket s, final BufferedReader br, final String params) throws IOException {
    final String[] parts = params.split("[,\\s]+");
    final int w = Integer.parseInt(parts[0]);
    final int h = Integer.parseInt(parts[1]);
    try {
      SwingUtilities.invokeAndWait(() -> {
        frame.resizeWindowTo(w, h);
      });
    } catch (InvocationTargetException | InterruptedException e) {
      e.printStackTrace();
    }

    markDoneAndWaitForAck(s, br);
  }

  // TODO: Add way to mark error
  private static void markDoneAndWaitForAck(final Socket s, final BufferedReader br) throws IOException {
    final DataOutputStream dos = new DataOutputStream(s.getOutputStream());
    dos.writeInt(0);
    dos.flush();

    // Wait for ACK
    br.readLine();
  }

  private class ReadyChecker implements Runnable {
    boolean isReady = false;
    @Override
    public void run() {
      isReady = frame.getComponentContent().isReadyToPaint();
    }
  }

  private boolean isReadyToPaint() {
    try {
      ReadyChecker checker = new ReadyChecker();
      SwingUtilities.invokeAndWait(checker);
      return checker.isReady;
    } catch (InvocationTargetException | InterruptedException e) {
      e.printStackTrace();
    }
    return true;
  }

  private void handleScreenShot(final Socket s, final BufferedReader br) throws IOException {
    try {
      while (!isReadyToPaint()) {
        Thread.sleep(50);
      }
    } catch (InterruptedException e) {
      e.printStackTrace();
    }

    final ComponentContent componentContent = frame.getComponentContent();
    componentContent.disableRenderHints();

    final Component component = componentContent.getComponent();
    final BufferedImage img = new BufferedImage(component.getWidth(), component.getHeight(), BufferedImage.TYPE_INT_ARGB);
    Graphics g = img.getGraphics();
    try {
      SwingUtilities.invokeAndWait(() -> {
        component.paint(g);
      });
    } catch (InvocationTargetException | InterruptedException e) {
      e.printStackTrace();
    }
    final ByteArrayOutputStream bos = new ByteArrayOutputStream();
    ImageIO.write(img, "PNG", bos);
    final OutputStream os = s.getOutputStream();
    {
      final DataOutputStream dos = new DataOutputStream(os);
      dos.writeInt(bos.size());
      dos.flush();
    }
    bos.writeTo(os);
    os.flush();

    // Wait for ACK
    br.readLine();
  }

  private void handleTo(final Socket s, final BufferedReader br, final String path)
      throws MalformedURLException, InterruptedException, ExecutionException, IOException {
    // System.out.println("  path: " + path);
    frame.setProgressEvent(null);
    frame.navigate(path);
    waitForNavigationCompletion();

    // TODO: reload and wait for navigation after enabling all requests?
    frame.allowAllFirstPartyRequests();

    markDoneAndWaitForAck(s, br);
  }

  private void waitForNavigationCompletion() throws InterruptedException, ExecutionException {
    {
      NavigatorProgressEvent progressEvent = frame.getProgressEvent();
      while (progressEvent == null || progressEvent.getProgressType() != ProgressType.DONE) {
        Thread.sleep(10);
        progressEvent = frame.getProgressEvent();
      }

      while (frame.getComponentContent() == null) {
        Thread.sleep(10);
      }

      final Component component = frame.getComponentContent().getComponent();
      if (component instanceof DefferedLayoutSupport) {
        final DefferedLayoutSupport defSupport = (DefferedLayoutSupport) component;
        defSupport.layoutCompletion().get();
      }
    }
  }

  public int getPort() {
    return socket.getLocalPort();
  }
}