using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Drawing;


namespace BalloonatixBuilder
{
    class Run
    {
        public byte Value { get; protected set; }
        public int Count { get; protected set; }


        public Run(byte b, int count)
        {
            Value = b;
            Count = count;
        }
    }


    class Program
    {
        static void Main(string[] args)
        {
            const string IN_FOLDER = @"C:\Users\Waterman\Desktop\balloonatixbuilder";
            const string OUT_FILE = @"C:\Users\Waterman\Desktop\balloonatixbuilder\test_output.tap";

            List<byte> mapsBasic = new List<byte>();
            int lineNumber = 10;

            foreach (string inFile in Directory.GetFiles(IN_FOLDER, "map*.ico"))
            {
                using (Bitmap img = (Bitmap) Image.FromFile(inFile))
                {
                    Point[] starts, goals;
                    var pix = GetAllMapPixelsFromBitmap(img, out starts, out goals);
                    var runs = RunLengthEncode(pix, 32);

                    mapsBasic.AddRange(BuildBasicForMapData(lineNumber, starts, goals, runs));
                    lineNumber += 2;
                }
            }

            using (FileStream stream = new FileStream(OUT_FILE, FileMode.Create, FileAccess.Write))
            using (BinaryWriter w = new BinaryWriter(stream))
            {
                // Each block in a TAP file starts with two bytes specifying how many bytes will follow.
                // Raw tape data follows, preceded by a block flag and followed by a checksum byte.
                // The checksum is the bitwise XOR of every byte from block flag onwards.
                // Note that each file on tape comprises two blocks: header and data.

                // Header block

                var b = new List<byte>();

                b.AddRange(ZxTools.EncodeToLittleEndianPair(19)); // block length incl. header and checksum bytes
                b.Add((byte) ZxTapeBlockType.Header);
                b.Add((byte) ZxTapeFileType.Program); // data type
                b.AddRange(ZxTools.EncodeString("x".PadRight(ZxTools.TAPE_FILENAME_LENGTH, ' '))); // filename
                b.AddRange(ZxTools.EncodeToLittleEndianPair(mapsBasic.Count)); // length
                b.AddRange(ZxTools.EncodeToLittleEndianPair(ZxTools.TAPE_PARAM1_BASIC_NO_AUTORUN)); // param 1: no BASIC auto-run
                b.AddRange(ZxTools.EncodeToLittleEndianPair(mapsBasic.Count)); // param 2: "start of variable area relative to start of program": Dunny says use program length
                b.Add(ZxTools.GenerateTapeChecksum(b.ToArray(), 2, 18));

                w.Write(b.ToArray());

                // Data block

                b.Clear();

                b.AddRange(ZxTools.EncodeToLittleEndianPair(mapsBasic.Count + 2));
                b.Add((byte) ZxTapeBlockType.Data);
                b.AddRange(mapsBasic);
                b.Add(ZxTools.GenerateTapeChecksum(b.ToArray(), 2, b.Count - 2));

                w.Write(b.ToArray());
            }
        }


        static byte[] BuildBasicForMapData(int lineNumber, Point[] starts, Point[] goals, IEnumerable<Run> runs)
        {
            var bb = new BasicBuilder();

            var x = new List<byte>();
            x.Add((byte) ZxKeyword.Data);

            // Begin with the two starts and two goals.

            x.AddRange(BasicBuilder.EncodeInteger(starts[0].X));
            x.Add((byte) ',');
            x.AddRange(BasicBuilder.EncodeInteger(starts[0].Y));
            x.Add((byte) ',');
            x.AddRange(BasicBuilder.EncodeInteger(starts[1].X));
            x.Add((byte) ',');
            x.AddRange(BasicBuilder.EncodeInteger(starts[1].Y));
            x.Add((byte) ',');
            x.AddRange(BasicBuilder.EncodeInteger(goals[0].X));
            x.Add((byte) ',');
            x.AddRange(BasicBuilder.EncodeInteger(goals[0].Y));
            x.Add((byte) ',');
            x.AddRange(BasicBuilder.EncodeInteger(goals[1].X));
            x.Add((byte) ',');
            x.AddRange(BasicBuilder.EncodeInteger(goals[1].Y));
            x.Add((byte) ',');

            // Now the run-length encoded map layout.

            foreach (Run r in runs)
            {
                x.AddRange(BasicBuilder.EncodeInteger(r.Count));
                x.Add((byte) ',');
                x.AddRange(BasicBuilder.EncodeInteger(r.Value));
                x.Add((byte) ',');
            }

            x.RemoveAt(x.Count() - 1); // trailing comma

            bb.AddLine(lineNumber, x);

            return bb.ToByteArray();
        }


        private static List<byte> GetAllMapPixelsFromBitmap(Bitmap img, out Point[] starts, out Point[] goals)
        {
            List<Point> theStarts = new List<Point>(), theGoals = new List<Point>();

            var inksForColors = new Dictionary<int, byte>();
            inksForColors.Add(0x000000, 0); // sky
            inksForColors.Add(0x0000ff, 1); // spike v
            inksForColors.Add(0xff0000, 2); // wall
            inksForColors.Add(0xff00ff, 3); // spike >
            inksForColors.Add(0x00ff00, 4); // spike <
            inksForColors.Add(0x00ffff, 5); // spike ^
            //inksForColors.Add(0xffff00, 6);  // not used
            inksForColors.Add(0xffffff, 7); // goal point (white)
            inksForColors.Add(0x808080, 8); // start point (grey)

            // 32x32 icon, but rows 0 and 22+ are not used for our map.

            var pix = new List<byte>();

            for (int y = 1; y <= 21; y++)
            {
                for (int x = 0; x <= 31; x++)
                {
                    Color c = img.GetPixel(x, y);
                    int a = c.B + c.G * 256 + c.R * 65536;
                    var ink = inksForColors[a];

                    if (ink == 8) // grey indicates a start point (top-left corner of the 2x2), so map it to sky but store it for later
                    {
                        theStarts.Add(new Point(x, y));
                        ink = 0;
                    }
                    else if (ink == 7) // similarly, white indicates a goal point
                    {
                        theGoals.Add(new Point(x, y));
                        ink = 0;
                    }
                    
                    pix.Add(ink);
                }
            }

            if (theStarts.Count != 2 || theGoals.Count != 2) throw new ApplicationException();

            starts = theStarts.ToArray();
            goals = theGoals.ToArray();

            return pix;
        }


        private static List<Run> RunLengthEncode(List<byte> c, int maxRun)
        {
            const int NONE = 255;

            List<Run> runs = new List<Run>();

            byte curColour = NONE;
            int curCount = 0;

            for (int i = 0; i < c.Count; i++)
            {
                if (c[i] == curColour && curCount < maxRun)
                {
                    curCount++;
                    continue;
                }
                else
                {
                    if (curColour != NONE)
                    {
                        runs.Add(new Run(curColour, curCount));
                    }

                    curColour = c[i];
                    curCount = 1;
                }
            }

            if (curCount > 0) runs.Add(new Run(curColour, curCount));

            return runs;
        }
    }


    enum ZxTapeFileType : byte
	{
		Program = 0, NumberArray = 1, CharArray = 2, Code = 3
	}

    enum ZxTapeBlockType : byte
	{
        Header = 0, Data = 255
	}


    static class ZxTools
    {
        public const int TAPE_FILENAME_LENGTH = 10;
        
        public const int TAPE_PARAM1_BASIC_NO_AUTORUN = 32768;


        public static byte[] EncodeToLittleEndianPair(int val)
        {
            byte num256s = (byte) (val / 256);
            byte remainder = (byte) (val - num256s * 256);

            return new byte[] { remainder, num256s };
        }

        
        public static byte[] EncodeToBigEndianPair(int val)
        {
            byte num256s = (byte) (val / 256);
            byte remainder = (byte) (val - num256s * 256);

            return new byte[] { num256s, remainder };
        }


        public static byte[] EncodeString(string s)
        {
            byte[] b = new byte[s.Length];

            for (int i = 0; i < s.Length; i++)
            {
                b[i] = (byte) s[i];
            }

            return b;
        }


        public static byte GenerateTapeChecksum(byte[] data, int start, int length)
        {
            byte check = 0;

            for (int i = start; i < start + length; i++)
            {
                check ^= data[i];
            }

            return check;
        }
    }


    enum ZxKeyword : byte
    {
        Spectrum = 163, Play, Rnd, InkeyS, Pi, Fn, Point, ScreenS, Attr, At, Tab, ValS, Code, Val, Len, Sin, Cos, Tan, Asn, Acs, Atn,
        Ln, Exp, Int, Sqr, Sgn, Abs, Peek, In, Usr, StrS, ChrS, Not, Bin, Or, And, LessOrEqual, GreaterOrEqual, NotEqual, Line, Then,
        To, Step, DefFn, Cat, Format, Move, Erase, Open, Close, Merge, Verify, Beep, Circle, Ink, Paper, Flash, Bright, Inverse, Over,
        Out, Lprint, Llist, Stop, Read, Data, Restore, New, Border, Continue, Dim, Rem, For, GoTo, GoSub, Input, Load, List, Let, Pause,
        Next, Poke, Print, Plot, Run, Save, Randomize, If, Cls, Draw, Clear, Return, Copy
    }


    class BasicBuilder
    {
        private const byte NEWLINE = 13;

        private const byte TYPE_FLOAT = 0x7e, TYPE_INT = 0x0e;

        private List<byte> _data = new List<byte>();


        public BasicBuilder()
        {
        }


        public byte[] ToByteArray()
        {
            return _data.ToArray();
        }


        public void AddLine(int number, IEnumerable<byte> contents)
        {
            _data.AddRange(ZxTools.EncodeToBigEndianPair(number));
            _data.AddRange(ZxTools.EncodeToLittleEndianPair(contents.Count() + 1)); // including the newline

            _data.AddRange(contents);

            _data.Add(NEWLINE);
        }


        public static byte[] EncodeInteger(int i)
        {
            // See: http://fileformats.archiveteam.org/wiki/Sinclair_BASIC_tokenized_file#5-byte_numeric_format

            var b = new List<byte>();

            b.AddRange(ZxTools.EncodeString(i.ToString()));
            b.Add(TYPE_INT);

            int baseValue = i;
            if (i < 0) baseValue -= 65536;

            b.Add(0); // reserved
            b.Add((byte) ((i >= 0) ? 0 : 0xff)); // sign
            b.AddRange(ZxTools.EncodeToLittleEndianPair(baseValue));
            b.Add(0); // reserved
            
            return b.ToArray();
        }
    }
}
