/*
 * Copyright (C) 2020 Mikhail Burakov. This file is part of Pui.
 *
 * Pui is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Pui 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Pui.  If not, see <https://www.gnu.org/licenses/>.
 */

#include <ctype.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <err.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>

enum {
  kNoSection = 0,
  kSectionElements,
  kSectionDefaultPalette,
  kSectionActivePalette
};

struct PuiElement {
  uint8_t x, y, w, h;
};

struct MappedFile {
  void* data;
  size_t size;
};

struct BitmapHeader {
  uint16_t magic;
  uint32_t size;
  uint16_t reserved[2];
  uint32_t offset;
} __attribute__((packed));

struct DibHeader {
  uint32_t size;
  int32_t width;
  int32_t height;
  uint16_t planes;
  uint16_t bpp;
} __attribute__((packed));

struct Bitmap {
  struct MappedFile file;
  struct BitmapHeader* bitmap_header;
  struct DibHeader* dib_header;
  uint8_t* ptr;
  int index;
};

static void MapFile(const char* file, struct MappedFile* result) {
  int fd = open(file, O_RDONLY);
  if (fd == -1) err(1, "Failed to open file");
  struct stat buf;
  if (fstat(fd, &buf)) err(1, "Failed to stat file");
  void* data = mmap(NULL, (size_t)buf.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
  close(fd);
  if (data == MAP_FAILED) err(1, "Failed to map file");
  result->data = data;
  result->size = (size_t)buf.st_size;
}

static void UnmapFile(struct MappedFile* result) {
  munmap(result->data, result->size);
}

static int ReadBitmap(struct Bitmap* bitmap) {
  uint8_t* buffer = bitmap->file.data;
  if (bitmap->ptr < buffer + bitmap->bitmap_header->offset) return -1;
  int result = bitmap->ptr[bitmap->index >> 1] >> ((~bitmap->index & 1) << 2);
  if (++bitmap->index == bitmap->dib_header->width) {
    size_t stride = ((size_t)(bitmap->dib_header->width >> 1) + 3) & ~3u;
    bitmap->ptr -= stride;
    bitmap->index = 0;
  }
  return result & 0x7;
}

static void OpenBitmap(const char* fname, struct Bitmap* bitmap) {
  MapFile(fname, &bitmap->file);
  uint8_t* data = bitmap->file.data;
  bitmap->bitmap_header = (struct BitmapHeader*)data;
  bitmap->dib_header = (struct DibHeader*)(data + sizeof(struct BitmapHeader));
  if (bitmap->bitmap_header->magic != 0x4d42) err(1, "Invalid bitmap magic");
  if (bitmap->dib_header->width < 0 || bitmap->dib_header->height < 0 ||
      bitmap->dib_header->width > UINT16_MAX ||
      bitmap->dib_header->height > UINT16_MAX ||
      bitmap->dib_header->planes != 1 || bitmap->dib_header->bpp != 4)
    err(1, "Unsupported bitmap format");
  size_t stride = ((size_t)(bitmap->dib_header->width >> 1) + 3) & ~3u;
  bitmap->ptr = data + bitmap->bitmap_header->offset +
                stride * (size_t)(bitmap->dib_header->height - 1);
}

static void Write(int item, FILE* out) {
  static int current = -1;
  if (current == -1) {
    if (item == -1) return;
    current = (item & 0xf) << 4;
  } else {
    if (item == -1) item = 0;
    current |= item & 0xf;
    fputc(current, out);
    current = -1;
  }
}

static void WriteCounter(int counter, FILE* out) {
  int pos = 0;
  while (counter >> (++pos * 3))
    ;
  while (pos-- > 0) Write(0x8 | counter >> (pos * 3), out);
}

static void WriteSequence(int prev, int counter, FILE* out) {
  switch (counter) {
    case 1:
      Write(prev, out);
      break;
    case 2:
      Write(prev, out);
      Write(prev, out);
      break;
    default:
      WriteCounter(counter - 2, out);
      Write(prev, out);
      break;
  }
}

static void SelectScale(int32_t size, uint8_t* value, uint8_t* scale) {
  if (size < 0) err(1, "Size must be non-negative");
  for (int shift = 0, mask = 0;; shift++, mask = mask << 1 | 1) {
    if (size & mask) err(1, "Size is not aligned");
    if (size < 0x100 << shift) {
      *value = (uint8_t)(size >> shift);
      *scale = (uint8_t)shift;
      return;
    }
  }
}

static int IsMeaningless(int ch) { return !isgraph(ch); }
static int IsMeaningful(int ch) { return isprint(ch); }

static void SkipGroup(struct MappedFile* file, int (*pred)(int ch)) {
  char* ptr = file->data;
  size_t size = file->size;
  for (; size && pred(*ptr); ptr++, size--)
    ;
  file->data = ptr;
  file->size = size;
}

static int ChangeSection(struct MappedFile* file) {
  char* begin = file->data;
  SkipGroup(file, IsMeaningful);
  size_t size = (size_t)((char*)file->data - begin);
  static const char kElements[] = {'[', 'E', 'l', 'e', 'm',
                                   'e', 'n', 't', 's', ']'};
  static const char kDefaultPalette[] = {'[', 'D', 'e', 'f', 'a', 'u',
                                         'l', 't', 'P', 'a', 'l', 'e',
                                         't', 't', 'e', ']'};
  static const char kActivePalette[] = {'[', 'A', 'c', 't', 'i', 'v', 'e', 'P',
                                        'a', 'l', 'e', 't', 't', 'e', ']'};
  switch (size) {
    case sizeof(kElements):
      if (!memcmp(begin, kElements, sizeof(kElements))) return kSectionElements;
      break;
    case sizeof(kDefaultPalette):
      if (!memcmp(begin, kDefaultPalette, sizeof(kDefaultPalette)))
        return kSectionDefaultPalette;
      break;
    case sizeof(kActivePalette):
      if (!memcmp(begin, kActivePalette, sizeof(kActivePalette)))
        return kSectionActivePalette;
      break;
    default:
      break;
  }
  return kNoSection;
}

static int GetIndex(char* ptr, size_t size, const char* prefix,
                    size_t prefix_length) {
  if (size < prefix_length + 3 || memcmp(ptr, prefix, prefix_length) ||
      !isdigit(ptr[prefix_length]) || !isdigit(ptr[prefix_length + 1]) ||
      ptr[prefix_length + 2] != '=')
    return -1;
  return atoi(ptr + prefix_length);
}

static int ParseElement(struct MappedFile* file, uint8_t scale[2],
                        struct PuiElement* elements) {
  char* ptr = file->data;
  SkipGroup(file, IsMeaningful);
  size_t size = (size_t)((char*)file->data - ptr);
  static const char kElement[] = {'E', 'l', 'e', 'm', 'e', 'n', 't'};
  int index = GetIndex(ptr, size, kElement, sizeof(kElement));
  if (index == -1 || index > 14) return -1;
  ptr += sizeof(kElement) + 3;
  size -= sizeof(kElement) + 3;
  int values[] = {0, 0, 0, 0};
  int *value = values, *end = values + 4;
  for (; size; ptr++, size--) {
    if ('0' <= *ptr && *ptr <= '9') {
      *value = *value * 10 + *ptr - '0';
    } else if (*ptr == ',') {
      if (++value == end) break;
    } else {
      break;
    }
  }
  elements[index].x = (uint8_t)(values[0] >> scale[0]);
  elements[index].y = (uint8_t)(values[1] >> scale[1]);
  elements[index].w = (uint8_t)(values[2] >> scale[0]);
  elements[index].h = (uint8_t)(values[3] >> scale[1]);
  return index;
}

static int ParsePalette(struct MappedFile* file, uint32_t* palette) {
  char* ptr = file->data;
  SkipGroup(file, IsMeaningful);
  size_t size = (size_t)((char*)file->data - ptr);
  static const char kColor[] = {'C', 'o', 'l', 'o', 'r'};
  int index = GetIndex(ptr, size, kColor, sizeof(kColor));
  if (index == -1 || index > 7) return -1;
  ptr += sizeof(kColor) + 3;
  size -= sizeof(kColor) + 3;
  uint32_t value = 0;
  for (; size; ptr++, size--) {
    if ('0' <= *ptr && *ptr <= '9') {
      value = value << 4 | (unsigned)(*ptr - '0');
    } else if ('A' <= *ptr && *ptr <= 'F') {
      value = value << 4 | (unsigned)(*ptr - 'A' + 10);
    } else if ('a' <= *ptr && *ptr <= 'f') {
      value = value << 4 | (unsigned)(*ptr - 'a' + 10);
    } else {
      break;
    }
  }
  palette[index] = (value >> 24 & 0xff) | (value >> 8 & 0xff00) |
                   (value << 8 & 0xff0000) | (value << 24 & 0xff000000);
  return index;
}

static void CompressDefinition(const struct MappedFile* definition,
                               uint8_t scale[2], FILE* out) {
  struct PuiElement elements[0xf];
  memset(elements, 0, sizeof(elements));
  uint32_t palettes[2][8];
  memset(palettes, 0, sizeof(palettes));
  int section = kNoSection;
  int max_element = 0, max_color = 0;
  for (struct MappedFile def = *definition;;) {
    SkipGroup(&def, IsMeaningless);
    if (!def.size) break;
    switch (*(char*)def.data) {
      case '#':
        SkipGroup(&def, IsMeaningful);
        continue;
      case '[':
        section = ChangeSection(&def);
        continue;
      default:
        break;
    }
    switch (section) {
      case kSectionElements: {
        int index = ParseElement(&def, scale, elements);
        if (index > max_element) max_element = index;
        break;
      }
      case kSectionDefaultPalette: {
        int index = ParsePalette(&def, palettes[0]);
        if (index > max_color) max_color = index;
        break;
      }
      case kSectionActivePalette: {
        int index = ParsePalette(&def, palettes[1]);
        if (index > max_color) max_color = index;
        break;
      }
      default:
        SkipGroup(&def, IsMeaningful);
        break;
    }
  }
  int count_elements = max_element + 1;
  int count_colors = max_color + 1;
  uint8_t counter = (count_elements << 4 & 0xf0) | (count_colors & 0xf);
  fwrite(&counter, 1, 1, out);
  fwrite(elements, (size_t)count_elements * sizeof(struct PuiElement), 1, out);
  fwrite(palettes[0], (size_t)count_colors * sizeof(uint32_t), 1, out);
  fwrite(palettes[1], (size_t)count_colors * sizeof(uint32_t), 1, out);
}

int main(int argc, char** argv) {
  if (argc < 3) err(1, "Usage: %s [source.bmp] [definition.ini]", argv[0]);
  struct Bitmap bitmap;
  memset(&bitmap, 0, sizeof(bitmap));
  OpenBitmap(argv[1], &bitmap);
  uint8_t whs[3], scale[2];
  SelectScale(bitmap.dib_header->width, &whs[0], &scale[0]);
  SelectScale(bitmap.dib_header->height, &whs[1], &scale[1]);
  struct MappedFile definition;
  MapFile(argv[2], &definition);
  whs[2] = (uint8_t)(scale[0] << 4 | scale[1]);
  fwrite(whs, sizeof(whs), 1, stdout);
  CompressDefinition(&definition, scale, stdout);
  UnmapFile(&definition);
  for (int counter = 0, prev = -1;;) {
    int current = ReadBitmap(&bitmap);
    if (current == prev || prev == -1) {
      counter++;
    } else {
      WriteSequence(prev, counter, stdout);
      if (current == -1) break;
      counter = 1;
    }
    prev = current;
  }
  UnmapFile(&bitmap.file);
  Write(-1, stdout);
  return 0;
}