package me.srvstr.world;

import java.awt.Point;
import java.awt.geom.Point2D;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import javax.swing.JOptionPane;
import me.srvstr.bot.Direction;
import me.srvstr.bot.Gytebot;
import me.srvstr.objects.Coin;
import me.srvstr.objects.Wall;
import me.srvstr.objects.WorldObject;

public class World {
  public static final int MAP_SIZE = 32;
  private List<String> rawMapData;
  private int rawDataWidth;
  private int coinCount;
  private String author;
  private String version;
  private String name;
  private String date;
  private String mapPath;
  private volatile WorldObject[][] objects;
  private volatile Gytebot gytebot;
  private boolean updateCoinBuffer;
  private int[] coinBufferData;
  private boolean updateWallBufferData;
  private float[] wallBufferData;

  public World() {
    this.rawMapData = new LinkedList<>();

    this.coinBufferData = new int[1024];
    this.wallBufferData = new float[1024];

    setWorld("/data/standardlevel.map");
  }

  public final void setWorld(String fileName) {
    BufferedReader reader = null;

    try {
      reader = new BufferedReader(new InputStreamReader(getClass().getResourceAsStream(fileName)));
    } catch (Exception e1) {

      try {
        reader = new BufferedReader(new FileReader(new File(fileName)));
      } catch (Exception e2) {

        JOptionPane.showMessageDialog(null, "Unable to open Map: " + fileName, "Error - opening map", 0);
      }
    }

    if (reader != null) {

      try {

        this.gytebot = null;
        this.rawDataWidth = 0;
        this.rawMapData.clear();

        try {
          reader.lines().forEachOrdered(line -> parseLine(line));

          if (this.rawDataWidth > 0 && this.rawMapData.size() > 0) {

            this.objects = new WorldObject[this.rawDataWidth][this.rawMapData.size()];

            createWorldData();

            updateTransferBuffers();

            this.mapPath = fileName;

          } else {

            setWorld(this.mapPath);

            JOptionPane.showMessageDialog(null, "Unable to open Map: " + fileName, "Error - opening map", 0);
          }
        } catch (Error e) {

          setWorld(this.mapPath);

          JOptionPane.showMessageDialog(null, "Unable to open Map: " + fileName, "Error - opening map", 0);
        } finally {

          reader.close();
        }
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
  }

  public final void setDataString(String data) {
    this.gytebot = null;
    this.rawDataWidth = 0;
    this.rawMapData.clear();

    try {
      Arrays.<String>asList(data.split("(?=\\n)")).forEach(line -> parseLine(line));

      if (this.rawDataWidth > 0 && this.rawMapData.size() > 0) {

        this.objects = new WorldObject[this.rawDataWidth][this.rawMapData.size()];

        createWorldData();

        updateTransferBuffers();

      } else {

        setWorld(this.mapPath);
        JOptionPane.showMessageDialog(null, "The map was not properly loaded", "Error - converting map", 0);
      }
    } catch (Error e) {

      setWorld(this.mapPath);
      JOptionPane.showMessageDialog(null, "Unable to convert Map: " + e.getMessage(),
          "Error - converting map", 0);
    }
  }

  private void parseLine(String line) throws Error {
    String trimed = line.trim();

    if (trimed.length() != 0) {

      String[] splitted = trimed.split("[/](?=/)|(?=[@])");

      String rawMapLine = new String();
      byte b;
      int i;
      String[] arrayOfString1;
      for (i = (arrayOfString1 = splitted).length, b = 0; b < i;) {
        String content = arrayOfString1[b];

        if (content.startsWith("/")) {
          break;
        }

        if (content.startsWith("@")) {

          String[] tag = content.split("\\s+");

          if (tag.length > 2) {

            throw new AssertionError("To many arguments provided for tag: " + content);
          }

          String str;

          switch ((str = tag[0]).hashCode()) {
            case -440667701:
              if (str.equals("@author")) {
                this.author = tag[1];
                break;
              }
            case 62181358:
              if (str.equals("@date")) {
                this.date = tag[1];
                break;
              }
            case 62479051:
              if (str.equals("@name")) {
                this.name = tag[1];
                break;
              }
            case 222319768:
              if (str.equals("@version")) {
                this.version = tag[1];
                break;
              }
            default:
              throw new AssertionError("Unknown tag found: " + tag[0]);
          }

        } else {
          rawMapLine = String.valueOf(rawMapLine) + content;
        }
        b++;
      }

      if (rawMapLine.length() > 0) {
        this.rawMapData.add(rawMapLine);
      }

      this.rawDataWidth = Math.max(this.rawDataWidth, rawMapLine.length());

      if (this.rawDataWidth > 32) {
        throw new AssertionError("Map size exceeds valid map bounds of 32x32! It is to wide");
      }
      if (this.rawMapData.size() > 32) {
        throw new AssertionError("Map size exceeds valid map bounds of 32x32! It is to large");
      }
    }
  }

  private void createWorldData() {
    this.coinCount = 0;

    for (int y = 0; y < this.rawMapData.size(); y++) {

      for (int x = 0; x < ((String) this.rawMapData.get(y)).length(); x++) {

        char current = ((String) this.rawMapData.get(y)).charAt(x);

        if (current == '#') {

          this.objects[x][y] = (WorldObject) new Wall(new Point2D.Float(x, y));
        } else if (current == '€' || current == '$') {

          this.objects[x][y] = (WorldObject) new Coin(new Point2D.Float(x, y));

          this.coinCount++;
        } else if (current == '^' || current == '>' || current == 'v' || current == 'V' || current == '<') {

          if (this.gytebot != null) {

            System.err.println("Conflicting world rule: There is already a gytebot!");
            JOptionPane.showMessageDialog(null, "Conflicting world rule: There is already a gytebot",
                "Error - converting map", 0);

          } else if (x < ((String) this.rawMapData.get(0)).length() && y < this.rawMapData.size()) {

            this.gytebot = new Gytebot(new Point2D.Float(x, y), Direction.directionFromCodePoint(current));
          } else {

            this.gytebot = null;
            JOptionPane.showMessageDialog(null, "Gytebot is placed outside Map", "Error - converting map", 0);
          }

        } else if (current != ' ') {

          System.err.println("Unkown map entity: " + current);
        }
      }
    }
  }

  private void updateTransferBuffers() {
    Arrays.fill(this.coinBufferData, 0);
    Arrays.fill(this.wallBufferData, 0.0F);

    for (int y = 0; y < (this.objects[0]).length; y++) {
      for (int x = 0; x < this.objects.length; x++) {

        if (this.objects[x][y] instanceof Coin) {

          this.coinBufferData[x + y * this.objects.length] = 1;
        } else if (this.objects[x][y] instanceof Wall) {

          this.wallBufferData[x + y * 32] = 1.0F;
        }
      }
    }

    this.updateCoinBuffer = true;
    this.updateWallBufferData = true;
  }

  public final void setWall(int x, int y) {
    setObject(x, y, (WorldObject) new Wall(new Point2D.Float(x, y)));
  }

  public final void setCoin(int x, int y) {
    setObject(x, y, (WorldObject) new Coin(new Point2D.Float(x, y)));
  }

  public final void removeObject(int x, int y) {
    if (this.objects[x][y] != null) {

      this.objects[x][y] = null;

      updateTransferBuffers();

    } else {

      JOptionPane.showMessageDialog(null, "Index to remove object from is already empty", "Exception", 0);
    }
  }

  private void setObject(int x, int y, WorldObject object) {
    if (x >= 0 && x < this.objects.length && y >= 0 && y < (this.objects[0]).length) {

      this.objects[x][y] = object;

      updateTransferBuffers();
    } else {

      JOptionPane.showMessageDialog(null, "The requested object placement occured outside of valid bounds!",
          "Exception", 0);
    }
  }

  public boolean isLocationObstructed(Point point) {
    if (point.x < 0 || point.y < 0 || point.x >= getWidth() || point.y >= getHeight()) {
      return true;
    }
    if (this.objects[point.x][point.y] instanceof Wall) {
      return true;
    }

    return false;
  }

  public boolean isCoinPlacedHere(Point2D.Float location) {
    if (location.x < 0.0F || location.y < 0.0F || location.x >= getWidth() || location.y >= getHeight()) {
      return false;
    }
    if (this.objects[Math.round(location.x)][Math.round(location.y)] instanceof Coin) {
      return true;
    }

    return false;
  }

  public final String toString() {
    return this.rawMapData.stream().reduce("", (A, B) -> String.valueOf(A) + B + System.lineSeparator());
  }

  public boolean updateWallBufferData() {
    if (this.updateWallBufferData) {

      this.updateWallBufferData = false;
      return true;
    }

    return false;
  }

  public boolean updateCoinTextureData() {
    if (this.updateCoinBuffer) {

      this.updateCoinBuffer = false;
      return true;
    }

    return false;
  }

  public final int getCoinCount() {
    return this.coinCount;
  }

  public final String getAuthor() {
    return this.author;
  }

  public final String getVersion() {
    return this.version;
  }

  public final String getName() {
    return this.name;
  }

  public final String getDate() {
    return this.date;
  }

  public final synchronized Gytebot getGytebot() {
    return this.gytebot;
  }

  public final int[] getCoinBufferData() {
    return this.coinBufferData;
  }

  public final int getWidth() {
    return this.objects.length;
  }

  public final int getHeight() {
    return (this.objects[0]).length;
  }

  public int getDrawGytebotAsInt() {
    return (this.gytebot == null) ? 0 : 1;
  }

  public int[] getCoinTextureData() {
    return this.coinBufferData;
  }

  public float[] getWallBufferData() {
    return this.wallBufferData;
  }
}