/**
 * Copyright (c) 2008-2021 Bird Dog Games, Inc.
 *
 * This file is part of Ardor3D.
 *
 * Ardor3D is free software: you can redistribute it and/or modify it
 * under the terms of its license which may be found in the accompanying
 * LICENSE file or at <https://git.io/fjRmv>.
 */

package com.ardor3d.util.export.binary;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.GZIPInputStream;

import com.ardor3d.annotation.SavableFactory;
import com.ardor3d.math.util.MathUtils;
import com.ardor3d.util.Ardor3dException;
import com.ardor3d.util.export.Ardor3dImporter;
import com.ardor3d.util.export.ByteUtils;
import com.ardor3d.util.export.ReadListener;
import com.ardor3d.util.export.Savable;

public class BinaryImporter implements Ardor3dImporter {
  private static final Logger logger = Logger.getLogger(BinaryImporter.class.getName());

  // Key - alias, object - bco
  protected final Map<String, BinaryClassObject> _classes = new HashMap<>();
  // Key - id, object - the savable
  protected final Map<Integer, Savable> _contentTable = new HashMap<>();
  // Key - savable, object - capsule
  protected final Map<Savable, BinaryInputCapsule> _capsuleTable = new IdentityHashMap<>();
  // Key - id, opject - location in the file
  protected final Map<Integer, Integer> _locationTable = new HashMap<>();

  protected byte[] _dataArray = null;
  protected int _aliasWidth = 0;

  public BinaryImporter() {}

  @Override
  public Savable load(final InputStream is) throws IOException {
    return load(is, null, null);
  }

  public Savable load(final InputStream is, final ReadListener listener) throws IOException {
    return load(is, listener, null);
  }

  public Savable load(final InputStream is, final ReadListener listener, final ByteArrayOutputStream reuseableStream)
      throws IOException {
    try {
      final GZIPInputStream zis = new GZIPInputStream(is);
      BufferedInputStream bis = new BufferedInputStream(zis);
      final int numClasses = ByteUtils.readInt(bis);
      int bytes = 4;
      _aliasWidth = ((int) MathUtils.log(numClasses, 256) + 1);
      for (int i = 0; i < numClasses; i++) {
        final String alias = readString(bis, _aliasWidth);

        final int classLength = ByteUtils.readInt(bis);
        final String className = readString(bis, classLength);
        final BinaryClassObject bco = new BinaryClassObject();
        bco._alias = alias.getBytes();
        bco._className = className;

        final int fields = ByteUtils.readInt(bis);
        bytes += (8 + _aliasWidth + classLength);

        bco._nameFields = new HashMap<>(fields);
        bco._aliasFields = new HashMap<>(fields);
        for (int x = 0; x < fields; x++) {
          final byte fieldAlias = (byte) bis.read();
          final byte fieldType = (byte) bis.read();

          final int fieldNameLength = ByteUtils.readInt(bis);
          final String fieldName = readString(bis, fieldNameLength);
          final BinaryClassField bcf = new BinaryClassField(fieldName, fieldAlias, fieldType);
          bco._nameFields.put(fieldName, bcf);
          bco._aliasFields.put(fieldAlias, bcf);
          bytes += (6 + fieldNameLength);
        }
        _classes.put(alias, bco);
      }
      if (listener != null) {
        listener.readBytes(bytes);
      }

      final int numLocs = ByteUtils.readInt(bis);
      bytes = 4;

      for (int i = 0; i < numLocs; i++) {
        final int id = ByteUtils.readInt(bis);
        final int loc = ByteUtils.readInt(bis);
        _locationTable.put(id, loc);
        bytes += 8;
      }

      @SuppressWarnings("unused")
      final int numbIDs = ByteUtils.readInt(bis); // XXX: NOT CURRENTLY USED
      final int id = ByteUtils.readInt(bis);
      bytes += 8;
      if (listener != null) {
        listener.readBytes(bytes);
      }

      ByteArrayOutputStream baos = reuseableStream;
      if (baos == null) {
        baos = new ByteArrayOutputStream(bytes);
      } else {
        baos.reset();
      }
      int size = -1;
      final byte[] cache = new byte[4096];
      while ((size = bis.read(cache)) != -1) {
        baos.write(cache, 0, size);
        if (listener != null) {
          listener.readBytes(size);
        }
      }
      bis = null;

      _dataArray = baos.toByteArray();
      baos = null;

      final Savable rVal = readObject(id);

      if (logger.isLoggable(Level.FINE)) {
        logger.fine("Importer Stats: ");
        logger.fine("Tags: " + numClasses);
        logger.fine("Objects: " + numLocs);
        logger.fine("Data Size: " + _dataArray.length);
      }
      return rVal;

    } finally {
      // Let go of / reset contents.
      _aliasWidth = 0;
      _contentTable.clear();
      _classes.clear();
      _capsuleTable.clear();
      _locationTable.clear();
      _dataArray = null;
    }
  }

  @Override
  public Savable load(final URL url) throws IOException {
    return load(url, null);
  }

  public Savable load(final URL url, final ReadListener listener) throws IOException {
    final InputStream is = url.openStream();
    final Savable rVal = load(is, listener);
    is.close();
    return rVal;
  }

  @Override
  public Savable load(final File file) throws IOException {
    return load(file, null);
  }

  public Savable load(final File file, final ReadListener listener) throws IOException {
    final FileInputStream fis = new FileInputStream(file);
    final Savable rVal = load(fis, listener);
    fis.close();
    return rVal;
  }

  @Override
  public Savable load(final byte[] data) throws IOException {
    final ByteArrayInputStream bais = new ByteArrayInputStream(data);
    final Savable rVal = load(bais);
    bais.close();
    return rVal;
  }

  protected String readString(final InputStream is, final int length) throws IOException {
    final byte[] data = new byte[length];
    is.read(data, 0, length);
    return new String(data);
  }

  protected String readString(final int length, final int offset) throws IOException {
    final byte[] data = new byte[length];
    for (int j = 0; j < length; j++) {
      data[j] = _dataArray[j + offset];
    }

    return new String(data);
  }

  public Savable readObject(final int id) {

    if (_contentTable.get(id) != null) {
      return _contentTable.get(id);
    }

    try {
      int loc = _locationTable.get(id);

      final String alias = readString(_aliasWidth, loc);
      loc += _aliasWidth;

      final BinaryClassObject bco = _classes.get(alias);

      if (bco == null) {
        logger.logp(Level.SEVERE, this.getClass().toString(), "readObject(int id)", "NULL class object: " + alias);
        return null;
      }

      final int dataLength = ByteUtils.convertIntFromBytes(_dataArray, loc);
      loc += 4;

      final BinaryInputCapsule cap = new BinaryInputCapsule(this, bco);
      cap.setContent(_dataArray, loc, loc + dataLength);

      final Savable out;

      try {
        @SuppressWarnings("unchecked")
        final Class<? extends Savable> clazz = (Class<? extends Savable>) Class.forName(bco._className);
        final SavableFactory ann = clazz.getAnnotation(SavableFactory.class);
        if (ann == null) {
          out = clazz.getDeclaredConstructor().newInstance();
        } else {
          out = (Savable) clazz.getMethod(ann.factoryMethod(), (Class<?>[]) null).invoke(null, (Object[]) null);
        }
      } catch (final InstantiationException e) {
        logger.logp(Level.SEVERE, this.getClass().toString(), "readObject(int)",
            "Could not access constructor of class '" + bco._className + "'! \n"
                + "Some types may require the annotation SavableFactory.  Please double check.",
            e);
        throw new Ardor3dException(e);
      } catch (final NoSuchMethodException e) {
        logger.logp(Level.SEVERE, this.getClass().toString(), "readObject(int)", e.getMessage() + " \n"
            + "Method specified in annotation does not appear to exist or has an invalid method signature.", e);
        throw new Ardor3dException(e);
      }

      _capsuleTable.put(out, cap);
      _contentTable.put(id, out);

      out.read(_capsuleTable.get(out));

      _capsuleTable.remove(out);

      return out;

    } catch (final Ardor3dException e) {
      // rethrow our own exceptions
      throw e;
    } catch (final Exception e) {
      logger.logp(Level.SEVERE, this.getClass().toString(), "readObject(int)", "Exception", e);
      throw new Ardor3dException(e);
    }
  }
}
