package com.riiablo.console;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.InputProcessor;
import com.badlogic.gdx.graphics.Pixmap;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.Batch;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.GlyphLayout;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.Disposable;
import com.badlogic.gdx.utils.Pools;
import com.badlogic.gdx.utils.Timer;
import com.riiablo.Cvars;
import com.riiablo.Keys;
import com.riiablo.Riiablo;
import com.riiablo.cvar.Cvar;
import com.riiablo.cvar.CvarStateAdapter;

import org.apache.commons.io.output.TeeOutputStream;
import org.apache.commons.lang3.Validate;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;

public class RenderedConsole extends Console implements Disposable, InputProcessor {
  private static final String TAG = "RenderedConsole";

  private static final String BUFFER_PREFIX = ">";

  private final Array<String>         OUTPUT = new Array<>();
  private final ByteArrayOutputStream BUFFER = new ByteArrayOutputStream();

  private final Array<String> HISTORY = new Array<>(64);
  private int historyIndex;

  private BitmapFont font;

  private Texture modalBackground;
  private Texture hintBackground;
  private Texture cursorTexture;

  private boolean visible;
  private float   height;
  private float   clientWidth, clientHeight;
  private float   textHeight;
  private float   lineHeight;
  private int     consoleHeight;
  private float   outputHeight;
  private float   consoleY;
  private float   bufferY;
  private float   outputY;
  private int     scrollOffset;
  private int     scrollOffsetMin;


  private static final float CARET_BLINK_DELAY = 0.5f;
  private static final float CARET_HOLD_DELAY  = 1.0f;
  private Timer.Task caretBlinkTask;
  private boolean showCaret;

  public static RenderedConsole wrap(OutputStream out) {
    ConsoleOutputStream cout = new ConsoleOutputStream();
    out = new TeeOutputStream(out, cout);
    RenderedConsole console = new RenderedConsole(out);
    cout.bind(console);
    return console;
  }

  RenderedConsole(OutputStream out) {
    super(out);
  }

  private void recalculateScrollOffset() {
    if (font == null) {
      return;
    }

    clientWidth     = Riiablo.client.width();
    clientHeight    = Riiablo.client.height();
    lineHeight      = font.getLineHeight();
    textHeight      = font.getCapHeight();
    consoleHeight   = (int) (clientHeight * height);
    consoleY        = clientHeight - consoleHeight;
    bufferY         = consoleY + textHeight;
    outputY         = bufferY + lineHeight;
    outputHeight    = consoleHeight - lineHeight - textHeight;
    scrollOffsetMin = (int) (outputHeight / lineHeight) + 1;
    scrollOffset    = Math.max(scrollOffset, scrollOffsetMin);
  }

  public BitmapFont getFont() {
    return font;
  }

  public boolean isVisible() {
    return visible;
  }

  public void setVisible(boolean b) {
    if (b != visible) {
      visible = b;
      updateCaret();
      Gdx.input.setOnscreenKeyboardVisible(b);
      scrollOffset = OUTPUT.size;
    }
  }

  @Override
  public void clear() {
    OUTPUT.clear();
  }

  public void create() {
    Pixmap solidColorPixmap = new Pixmap(1, 1, Pixmap.Format.RGBA8888);
    solidColorPixmap.setColor(0.0f, 0.0f, 0.0f, 0.5f);
    solidColorPixmap.fill();
    modalBackground = new Texture(solidColorPixmap);
    solidColorPixmap.dispose();

    solidColorPixmap = new Pixmap(1, 1, Pixmap.Format.RGBA8888);
    solidColorPixmap.setColor(1.0f, 1.0f, 1.0f, 1.0f);
    solidColorPixmap.fill();
    cursorTexture = new Texture(solidColorPixmap);
    solidColorPixmap.dispose();

    final Cvar.StateListener<Float> colorChangeListener = new CvarStateAdapter<Float>() {
      @Override
      public void onChanged(Cvar<Float> cvar, Float from, Float to) {
        if (cvar == Cvars.Client.Console.Color.a) {
          font.getColor().a = to;
        } else if (cvar == Cvars.Client.Console.Color.r) {
          font.getColor().r = to;
        } else if (cvar == Cvars.Client.Console.Color.g) {
          font.getColor().g = to;
        } else if (cvar == Cvars.Client.Console.Color.b) {
          font.getColor().b = to;
        }
      }
    };
    Cvars.Client.Console.Font.addStateListener(new CvarStateAdapter<String>() {
      @Override
      public void onChanged(Cvar<String> cvar, String from, String to) {
        if (from != null) Riiablo.assets.unload(from);
        Riiablo.assets.load(to, BitmapFont.class);
        Riiablo.assets.finishLoadingAsset(to);
        font = Riiablo.assets.get(to);
        Cvars.Client.Console.Color.a.addStateListener(colorChangeListener);
        Cvars.Client.Console.Color.r.addStateListener(colorChangeListener);
        Cvars.Client.Console.Color.g.addStateListener(colorChangeListener);
        Cvars.Client.Console.Color.b.addStateListener(colorChangeListener);
        recalculateScrollOffset();
      }
    });

    Cvars.Client.Console.Height.addStateListener(new CvarStateAdapter<Float>() {
      @Override
      public void onChanged(Cvar<Float> cvar, Float from, Float to) {
        height = to;
        recalculateScrollOffset();
      }
    });

    caretBlinkTask = new Timer.Task() {
      @Override
      public void run() {
        showCaret = !showCaret;
      }
    };

    in.clear();
    updateCaret();
  }

  @Override
  protected void onCaretMoved(int position) {
    updateCaret();
  }

  private void updateCaret() {
    caretBlinkTask.cancel();
    Timer.schedule(caretBlinkTask, CARET_HOLD_DELAY, CARET_BLINK_DELAY);
    showCaret = true;
  }

  public void resize(int width, int height) {
    recalculateScrollOffset();
  }

  public void render(Batch b) {
    if (!visible || font == null) return;

    b.draw(modalBackground, 0, consoleY - 4, clientWidth, consoleHeight + 4);

    final int x = 2;
    String inputContents = in.getContents();
    GlyphLayout glyphs = font.draw(b, BUFFER_PREFIX + inputContents, x, bufferY - 2);
    b.draw(cursorTexture, x, bufferY, clientWidth, 2);
    if (showCaret) {
      final int caret = in.getCaretPosition();
      if (caret != in.length()) {
        glyphs.setText(font, BUFFER_PREFIX + inputContents.substring(0, caret));
      }

      b.draw(cursorTexture, x + glyphs.width, consoleY - 2, 2, textHeight);
    }
    Pools.free(glyphs);

    final float outputOffset = scrollOffset * lineHeight;
    if (outputOffset < outputHeight) {
      // offsets output to always appear that it starts at top of console window
      scrollOffset = Math.max(scrollOffset, scrollOffsetMin);
    }

    float position = outputY;
    final int outputSize = OUTPUT.size;
    if (scrollOffset > outputSize) {
      scrollOffset = outputSize;
      position += ((scrollOffsetMin - scrollOffset) * lineHeight);
    }

    for (int i = scrollOffset - 1; i >= 0; i--) {
      if (position > clientHeight) break;
      String line = OUTPUT.get(i);
      font.draw(b, line, x, position);
      position += lineHeight;
    }
  }

  @Override
  public void dispose() {
    cursorTexture.dispose();
    modalBackground.dispose();
  }

  @Override
  protected void onInputCommit(String buffer) {
    HISTORY.add(buffer);
    historyIndex = HISTORY.size;
  }

  @Override
  public boolean keyDown(int keycode) {
    if (Keys.Console.isAssigned(keycode)
        || ((Gdx.input.isKeyPressed(Input.Keys.VOLUME_UP) || Gdx.input.isKeyPressed(Input.Keys.VOLUME_DOWN))
            && Gdx.input.isKeyPressed(Input.Keys.BACK))) {
      visible = !visible;
      return true;
    } else if (!visible) {
      return false;
    }

    switch (keycode) {
      case Input.Keys.MENU:
      case Input.Keys.ESCAPE:
      case Input.Keys.BACK:
        setVisible(true);
        return true;
      case Input.Keys.UP:
        if (historyIndex > 0) {
          in.set(HISTORY.get(--historyIndex));
        }
        return true;
      case Input.Keys.DOWN:
        if (historyIndex < HISTORY.size) {
          in.set(HISTORY.get(historyIndex++));
        } else {
          in.clear();
        }
        return true;
      default:
        super.keyDown(keycode);
        return true;
    }
  }

  @Override
  public boolean keyUp(int keycode) {
    if (!visible) return false;
    super.keyUp(keycode);
    return true;
  }

  @Override
  public boolean keyTyped(char ch) {
    if (!visible) return false;
    if (Keys.Console.isAssigned(Input.Keys.valueOf(Character.toString(ch)))) {
      return true;
    }

    super.keyTyped(ch);
    return true;
  }

  @Override
  public boolean scrolled(int amount) {
    if (!visible) return false;
    switch (amount) {
      case -1:
        if (scrollOffset > 0) {
          scrollOffset--;
        }
        break;
      case 1:
        if (scrollOffset < OUTPUT.size) {
          scrollOffset++;
        }
        break;
      default:
        Gdx.app.error(TAG, "Unexpected scroll amount: " + amount);
    }

    super.scrolled(amount);
    return true;
  }

  @Override
  public boolean touchDown(int screenX, int screenY, int pointer, int button) {
    if (!visible) return false;
    Gdx.input.setOnscreenKeyboardVisible(true);
    super.touchDown(screenX, screenY, pointer, button);
    return true;
  }

  @Override
  public boolean touchUp(int screenX, int screenY, int pointer, int button) {
    if (!visible) return false;
    super.touchUp(screenX, screenY, pointer, button);
    return true;
  }

  @Override
  public boolean touchDragged(int screenX, int screenY, int pointer) {
    if (!visible) return false;
    super.touchDragged(screenX, screenY, pointer);
    return true;
  }

  @Override
  public boolean mouseMoved(int screenX, int screenY) {
    if (!visible) return false;
    super.mouseMoved(screenX, screenY);
    return true;
  }

  static class ConsoleOutputStream extends OutputStream {
    RenderedConsole console;

    void bind(RenderedConsole console) {
      Validate.validState(this.console == null, "already bound to " + this.console);
      this.console = console;
    }

    @Override
    public void write(int b) throws IOException {
      console.BUFFER.write(b);
      if (b == '\n') flush();
    }

    @Override
    public void write(byte[] b, int off, int len) throws IOException {
      console.BUFFER.write(b, off, len);
      int size = off + len;
      for (int i = off; i < size; i++) {
        if (b[i] == '\n') flush();
      }
    }

    @Override
    public void flush() throws IOException {
      console.OUTPUT.add(console.BUFFER.toString("UTF-8"));
      console.BUFFER.reset();
      int size = console.OUTPUT.size;
      if (console.scrollOffset == size - 1) {
        console.scrollOffset = size;
      }
    }
  }
}