import 'package:archive/archive.dart';
import 'package:xml/xml.dart' as XML;

import 'image.dart';
import 'image_exception.dart';
import 'formats/png_decoder.dart';

/**
 * Decode a [BitmapFont] from the contents of a zip file that stores the
 * .fnt font definition and associated PNG images.
 */
BitmapFont readFontZip(List<int> bytes) =>
    new BitmapFont.fromZip(bytes);


/**
 * Decode a [BitmapFont] from the contents of [font] definition (.fnt) file,
 * and an [Image] that stores the font [map].
 */
BitmapFont readFont(String font, Image map) =>
    new BitmapFont.fromFnt(font, map);


/**
 * A bitmap font that can be used with [drawString] and [drawChar] functions.
 * You can generate a font files from a program
 * like: http://kvazars.com/littera
 */
class BitmapFont {
  String face = '';
  int size = 0;
  bool bold = false;
  bool italic = false;
  String charset = '';
  String unicode = '';
  int stretchH = 0;
  bool smooth = false;
  bool antialias = false;
  List<int> padding = [];
  List<int> spacing = [];
  bool outline = false;
  int lineHeight = 0;
  int base = 0;
  num scaleW = 0;
  num scaleH = 0;
  int pages = 0;
  bool packed = false;

  Map<int, BitmapFontCharacter> characters = {};
  Map<int, Map<int, int>> kernings = {};

  /**
   * Decode a [BitmapFont] from the contents of [font] definition (.fnt) file,
   * and an [Image] that stores the font [map].
   */
  BitmapFont.fromFnt(String fnt, Image page) {
    Map<int, Image> fontPages = { 0: page };

    XML.XmlDocument xml;

    if (fnt.startsWith('<font>')) {
      xml = XML.parse(fnt);
      if (xml == null) {
        throw new ImageException('Invalid font XML');
      }
    } else {
      xml = _parseTextFnt(fnt);
    }

    _parseFnt(xml, fontPages);
  }

  /**
   * Decode a [BitmapFont] from the contents of a zip file that stores the
   * .fnt font definition and associated PNG images.
   */
  BitmapFont.fromZip(List<int> fileData) {
    Archive arc = new ZipDecoder().decodeBytes(fileData);

    ArchiveFile font_file;
    for (int i = 0; i < arc.numberOfFiles(); ++i) {
      if (arc.fileName(i).endsWith('.fnt')) {
        font_file = arc.files[i];
        break;
      }
    }

    if (font_file == null) {
      throw new ImageException('Invalid font archive');
    }

    String font_str = new String.fromCharCodes(font_file.content);
    XML.XmlDocument xml;

    if (font_str.startsWith('<font>')) {
      xml = XML.parse(font_str);
      if (xml == null) {
        throw new ImageException('Invalid font XML');
      }
    } else {
      xml = _parseTextFnt(font_str);
    }

    _parseFnt(xml, {}, arc);
  }

  /**
   * Get the amount the writer x position should advance after drawing the
   * character [ch].
   */
  int characterXAdvance(String ch) {
    if (ch.isEmpty) {
      return 0;
    }
    int c = ch.codeUnits[0];
    if (!characters.containsKey(ch)) {
      return base ~/ 2;
    }
    return characters[c].xadvance;
  }


  Iterable<XML.XmlElement> _childElements(XML.XmlNode n) =>
    n.children.where((c) => c is XML.XmlElement).map((c) => c as XML.XmlElement);


  void _parseFnt(XML.XmlDocument xml, Map<int, Image> fontPages,
                 [Archive arc]) {
    if (xml.children.length != 1) {
      throw new ImageException('Invalid font XML');
    }

    var font = xml.children[0];

    for (var c in _childElements(font)) {
      String name = c.name.toString();
      if (name == 'info') {
        for (XML.XmlAttribute a in c.attributes) {
          switch (a.name.toString()) {
            case 'face':
              face = a.value;
              break;
            case 'size':
              size = int.parse(a.value);
              break;
            case 'bold':
              bold = (int.parse(a.value) == 1);
              break;
            case 'italic':
              italic = (int.parse(a.value) == 1);
              break;
            case 'charset':
              charset = a.value;
              break;
            case 'unicode':
              unicode = a.value;
              break;
            case 'stretchH':
              stretchH = int.parse(a.value);
              break;
            case 'smooth':
              smooth = (int.parse(a.value) == 1);
              break;
            case 'antialias':
              antialias = (int.parse(a.value) == 1);
              break;
            case 'padding':
              List<String> tk = a.value.split(',');
              padding = [];
              for (String t in tk) {
                padding.add(int.parse(t));
              }
              break;
            case 'spacing':
              List<String> tk = a.value.split(',');
              spacing = [];
              for (String t in tk) {
                spacing.add(int.parse(t));
              }
              break;
            case 'outline':
              outline = (int.parse(a.value) == 1);
              break;
          }
        }
      } else if (name == 'common') {
        for (XML.XmlAttribute a in c.attributes) {
          switch (a.name.toString()) {
            case 'lineHeight':
              lineHeight = int.parse(a.value);
              break;
            case 'base':
              base = int.parse(a.value);
              break;
            case 'scaleW':
              scaleW = int.parse(a.value);
              break;
            case 'scaleH':
              scaleH = int.parse(a.value);
              break;
            case 'pages':
              pages = int.parse(a.value);
              break;
            case 'packed':
              packed = (int.parse(a.value) == 1);
              break;
          }
        }
      } else if (name == 'pages') {
        for (var page in _childElements(c)) {
          int id = int.parse(page.getAttribute('id'));
          String filename = page.getAttribute('file');

          if (fontPages.containsKey(id)) {
            throw new ImageException('Duplicate font page id found: $id.');
          }

          if (arc != null) {
            ArchiveFile imageFile = _findFile(arc, filename);
            if (imageFile == null) {
              throw new ImageException('Font zip missing font page image '
                                       '$filename');
            }

            Image image = new PngDecoder().decodeImage(imageFile.content);

            fontPages[id] = image;
          }
        }
      } else if (name == 'kernings') {
        for (var kerning in _childElements(c)) {
          int first = int.parse(kerning.getAttribute('first'));
          int second = int.parse(kerning.getAttribute('second'));
          int amount = int.parse(kerning.getAttribute('amount'));

          if (!kernings.containsKey(first)) {
            kernings[first] = {};
          }
          kernings[first][second] = amount;
        }
      }
    }

    for (var c in _childElements(font)) {
      String name = c.name.toString();
      if (name == 'chars') {
        for (var char in _childElements(c)) {
          int id = int.parse(char.getAttribute('id'));
          int x = int.parse(char.getAttribute('x'));
          int y = int.parse(char.getAttribute('y'));
          int width = int.parse(char.getAttribute('width'));
          int height = int.parse(char.getAttribute('height'));
          int xoffset = int.parse(char.getAttribute('xoffset'));
          int yoffset = int.parse(char.getAttribute('yoffset'));
          int xadvance = int.parse(char.getAttribute('xadvance'));
          int page = int.parse(char.getAttribute('page'));
          int chnl = int.parse(char.getAttribute('chnl'));

          if (!fontPages.containsKey(page)) {
            throw new ImageException('Missing page image: $page');
          }

          Image fontImage = fontPages[page];

          BitmapFontCharacter ch = new BitmapFontCharacter(id, width, height,
              xoffset, yoffset, xadvance, page, chnl);

          characters[id] = ch;

          int x2 = x + width;
          int y2 = y + height;
          int pi = 0;
          Image image = ch.image;
          for (int yi = y; yi < y2; ++yi) {
            for (int xi = x; xi < x2; ++xi) {
              image[pi++] = fontImage.getPixel(xi, yi);
            }
          }
        }
      }
    }
  }

  XML.XmlDocument _parseTextFnt(String content) {
    var children = <XML.XmlNode>[];
    var pageList = <XML.XmlNode>[];
    var charList = <XML.XmlNode>[];
    var kerningList = <XML.XmlNode>[];
    var charsAttrs;
    var kerningsAttrs;

    List<String> lines = content.split('\n');

    for (String line in lines) {
      if (line.isEmpty) {
        continue;
      }

      List<String> tk = line.split(' ');
      switch (tk[0]) {
        case 'info':
          var attrs = _parseParameters(tk);
          var info = new XML.XmlElement(new XML.XmlName('info'), attrs, []);
          children.add(info);
          break;
        case 'common':
          var attrs = _parseParameters(tk);
          var node = new XML.XmlElement(new XML.XmlName('common'), attrs, []);
          children.add(node);
          break;
        case 'page':
          var attrs = _parseParameters(tk);
          var page = new XML.XmlElement(new XML.XmlName('page'), attrs, []);
          pageList.add(page);
          break;
        case 'chars':
          charsAttrs = _parseParameters(tk);
          break;
        case 'char':
          var attrs = _parseParameters(tk);
          var node = new XML.XmlElement(new XML.XmlName('char'), attrs, []);
          charList.add(node);
          break;
        case 'kernings':
          kerningsAttrs = _parseParameters(tk);
          break;
        case 'kerning':
          var attrs = _parseParameters(tk);
          var node = new XML.XmlElement(new XML.XmlName('kerning'), attrs, []);
          kerningList.add(node);
          break;
      }
    }

    if (charsAttrs != null || charList.isNotEmpty) {
      var node = new XML.XmlElement(new XML.XmlName('chars'), charsAttrs,
          charList);
      children.add(node);
    }

    if (kerningsAttrs != null || kerningList.isNotEmpty) {
      var node = new XML.XmlElement(new XML.XmlName('kernings'), kerningsAttrs,
          kerningList);
      children.add(node);
    }

    if (pageList.isNotEmpty) {
      var pages = new XML.XmlElement(new XML.XmlName('pages'), [], pageList);
      children.add(pages);
    }

    var xml = new XML.XmlElement(new XML.XmlName('font'), [], children);
    var doc = new XML.XmlDocument([xml]);

    return doc;
  }

  List<XML.XmlAttribute> _parseParameters(List<String> tk) {
    var params = <XML.XmlAttribute>[];
    for (int ti = 1; ti < tk.length; ++ti) {
      if (tk[ti].isEmpty) {
        continue;
      }
      List<String> atk = tk[ti].split('=');
      if (atk.length != 2) {
        continue;
      }

      // Remove all " characters
      atk[1] = atk[1].replaceAll('"', '');

      var a = new XML.XmlAttribute(new XML.XmlName(atk[0]), atk[1]);
      params.add(a);
    }
    return params;
  }

  static ArchiveFile _findFile(Archive arc, String filename) {
    for (ArchiveFile f in arc.files) {
      if (f.name == filename) {
        return f;
      }
    }
    return null;
  }
}

/**
 * A single character in a [BitmapFont].
 */
class BitmapFontCharacter {
  final int id;
  final int width;
  final int height;
  final int xoffset;
  final int yoffset;
  final int xadvance;
  final int page;
  final int channel;
  final Image image;

  BitmapFontCharacter(this.id, int width, int height,
                      this.xoffset, this.yoffset, this.xadvance, this.page,
                      this.channel) :
    this.width = width,
    this.height = height,
    image = new Image(width, height);

  String toString() {
    Map x = {'id': id, 'width': width, 'height': height, 'xoffset': xoffset,
             'yoffset': yoffset, 'xadvance': xadvance, 'page': page,
             'channel': channel};
    return 'Character $x';
  }
}
