package org.robolectric.res.android;

import static org.robolectric.res.android.Util.dtohl;
import static org.robolectric.res.android.Util.dtohs;
import static org.robolectric.res.android.Util.isTruthy;

import java.nio.ByteBuffer;
import org.robolectric.res.android.ResourceTypes.ResChunk_header;
import org.robolectric.res.android.ResourceTypes.ResStringPool_header;
import org.robolectric.res.android.ResourceTypes.ResTableStagedAliasEntry;
import org.robolectric.res.android.ResourceTypes.ResTableStagedAliasHeader;
import org.robolectric.res.android.ResourceTypes.ResTable_header;
import org.robolectric.res.android.ResourceTypes.ResTable_lib_entry;
import org.robolectric.res.android.ResourceTypes.ResTable_lib_header;
import org.robolectric.res.android.ResourceTypes.ResTable_package;
import org.robolectric.res.android.ResourceTypes.ResTable_type;
import org.robolectric.res.android.ResourceTypes.WithOffset;

// transliterated from
// https://android.googlesource.com/platform/frameworks/base/+/android-9.0.0_r12/libs/androidfw/ChunkIterator.cpp and
// https://android.googlesource.com/platform/frameworks/base/+/android-9.0.0_r12/libs/androidfw/include/androidfw/Chunk.h

// Helpful wrapper around a ResChunk_header that provides getter methods
// that handle endianness conversions and provide access to the data portion
// of the chunk.
class Chunk {

  // public:
  Chunk(ResChunk_header chunk) {
    this.device_chunk_ = chunk;
  }

  // Returns the type of the chunk. Caller need not worry about endianness.
  int type() {
    return dtohs(device_chunk_.type);
  }

  // Returns the size of the entire chunk. This can be useful for skipping
  // over the entire chunk. Caller need not worry about endianness.
  int size() {
    return dtohl(device_chunk_.size);
  }

  // Returns the size of the header. Caller need not worry about endianness.
  int header_size() {
    return dtohs(device_chunk_.headerSize);
  }

  // template <typename T, int MinSize = sizeof(T)>
  // T* header() {
  //   if (header_size() >= MinSize) {
  //     return reinterpret_cast<T*>(device_chunk_);
  //   }
  //   return nullptr;
  // }

  ByteBuffer myBuf() {
    return device_chunk_.myBuf();
  }

  int myOffset() {
    return device_chunk_.myOffset();
  }

  public WithOffset data_ptr() {
    return new WithOffset(device_chunk_.myBuf(), device_chunk_.myOffset() + header_size());
  }

  int data_size() {
    return size() - header_size();
  }

  // private:
  private ResChunk_header device_chunk_;

  public ResTable_header asResTable_header() {
    if (header_size() >= ResTable_header.SIZEOF) {
      return new ResTable_header(device_chunk_.myBuf(), device_chunk_.myOffset());
    } else {
      return null;
    }
  }

  public ResStringPool_header asResStringPool_header() {
    if (header_size() >= ResStringPool_header.SIZEOF) {
      return new ResStringPool_header(device_chunk_.myBuf(), device_chunk_.myOffset());
    } else {
      return null;
    }
  }

  public ResTable_package asResTable_package(int size) {
    if (header_size() >= size) {
      return new ResTable_package(device_chunk_.myBuf(), device_chunk_.myOffset());
    } else {
      return null;
    }
  }

  public ResTable_type asResTable_type(int size) {
    if (header_size() >= size) {
      return new ResTable_type(device_chunk_.myBuf(), device_chunk_.myOffset());
    } else {
      return null;
    }
  }

  public ResTable_lib_header asResTable_lib_header() {
    if (header_size() >= ResTable_lib_header.SIZEOF) {
      return new ResTable_lib_header(device_chunk_.myBuf(), device_chunk_.myOffset());
    } else {
      return null;
    }
  }

  public ResTable_lib_entry asResTable_lib_entry() {
    if (header_size() >= ResTable_lib_entry.SIZEOF) {
      return new ResTable_lib_entry(device_chunk_.myBuf(), device_chunk_.myOffset());
    } else {
      return null;
    }
  }

  public ResTableStagedAliasHeader asResTableStagedAliasHeader() {
    if (header_size() >= ResTableStagedAliasHeader.SIZEOF) {
      return new ResTableStagedAliasHeader(device_chunk_.myBuf(), device_chunk_.myOffset());
    } else {
      return null;
    }
  }

  public ResTableStagedAliasEntry asResTableStagedAliasEntry() {
    if (data_size() >= ResTableStagedAliasEntry.SIZEOF) {
      return new ResTableStagedAliasEntry(
          device_chunk_.myBuf(), device_chunk_.myOffset() + header_size());
    } else {
      return null;
    }
  }

  static class Iterator {
    private ResChunk_header next_chunk_;
    private int len_;
    private String last_error_;
    private boolean last_error_was_fatal_ = true;

    public Iterator(WithOffset buf, int itemSize) {
      this.next_chunk_ = new ResChunk_header(buf.myBuf(), buf.myOffset());
      this.len_ = itemSize;
    }

    boolean HasNext() { return !HadError() && len_ != 0; };
    // Returns whether there was an error and processing should stop
    boolean HadError() { return last_error_ != null; }
    String GetLastError() { return last_error_; }
    // Returns whether there was an error and processing should stop. For legacy purposes,
    // some errors are considered "non fatal". Fatal errors stop processing new chunks and
    // throw away any chunks already processed. Non fatal errors also stop processing new
    // chunks, but, will retain and use any valid chunks already processed.
    boolean HadFatalError() { return HadError() && last_error_was_fatal_; }

    Chunk Next() {
      assert (len_ != 0) : "called Next() after last chunk";

      ResChunk_header this_chunk = next_chunk_;

      // We've already checked the values of this_chunk, so safely increment.
      // next_chunk_ = reinterpret_cast<const ResChunk_header*>(
      //     reinterpret_cast<const uint8_t*>(this_chunk) + dtohl(this_chunk->size));
      int remaining = len_ - dtohl(this_chunk.size);
      if (remaining <= 0) {
        next_chunk_ = null;
      } else {
        next_chunk_ = new ResChunk_header(
            this_chunk.myBuf(), this_chunk.myOffset() + dtohl(this_chunk.size));
      }
      len_ -= dtohl(this_chunk.size);

      if (len_ != 0) {
        // Prepare the next chunk.
        if (VerifyNextChunkNonFatal()) {
          VerifyNextChunk();
        }
      }
      return new Chunk(this_chunk);
    }

    // Returns false if there was an error. For legacy purposes.
    boolean VerifyNextChunkNonFatal() {
      if (len_ < ResChunk_header.SIZEOF) {
        last_error_ = "not enough space for header";
        last_error_was_fatal_ = false;
        return false;
      }
      int size = dtohl(next_chunk_.size);
      if (size > len_) {
        last_error_ = "chunk size is bigger than given data";
        last_error_was_fatal_ = false;
        return false;
      }
      return true;
    }

    // Returns false if there was an error.
    boolean VerifyNextChunk() {
      // uintptr_t header_start = reinterpret_cast<uintptr_t>(next_chunk_);
      int header_start = next_chunk_.myOffset();

      // This data must be 4-byte aligned, since we directly
      // access 32-bit words, which must be aligned on
      // certain architectures.
      if (isTruthy(header_start & 0x03)) {
        last_error_ = "header not aligned on 4-byte boundary";
        return false;
      }

      if (len_ < ResChunk_header.SIZEOF) {
        last_error_ = "not enough space for header";
        return false;
      }

      int header_size = dtohs(next_chunk_.headerSize);
      int size = dtohl(next_chunk_.size);
      if (header_size < ResChunk_header.SIZEOF) {
        last_error_ = "header size too small";
        return false;
      }

      if (header_size > size) {
        last_error_ = "header size is larger than entire chunk";
        return false;
      }

      if (size > len_) {
        last_error_ = "chunk size is bigger than given data";
        return false;
      }

      if (isTruthy((size | header_size) & 0x03)) {
        last_error_ = "header sizes are not aligned on 4-byte boundary";
        return false;
      }
      return true;
    }
  }
}
