﻿using System;
using System.Globalization;

namespace Oni.Imaging
{
    internal struct Color : IEquatable<Color>
    {
        private byte b, g, r, a;

        public Color(byte r, byte g, byte b)
            : this(r, g, b, 255)
        {
        }

        public Color(byte r, byte g, byte b, byte a)
        {
            this.b = b;
            this.g = g;
            this.r = r;
            this.a = a;
        }

        public Color(Vector3 v)
        {
            r = (byte)(v.X * 255.0f);
            g = (byte)(v.Y * 255.0f);
            b = (byte)(v.Z * 255.0f);
            a = 255;
        }

        public Color(Vector4 v)
        {
            r = (byte)(v.X * 255.0f);
            g = (byte)(v.Y * 255.0f);
            b = (byte)(v.Z * 255.0f);
            a = (byte)(v.W * 255.0f);
        }

        public bool IsTransparent => a != 255;

        public byte R => r;
        public byte G => g;
        public byte B => b;
        public byte A => a;

        public int ToBgra32() => b | (g << 8) | (r << 16) | (a << 24);
        public int ToBgr565() => (b >> 3) | ((g & 0xfc) << 3) | ((r & 0xf8) << 8);

        public Vector3 ToVector3() => new Vector3(r, g, b) / 255.0f;
        public Vector4 ToVector4() => new Vector4(r, g, b, a) / 255.0f;

        public static bool operator ==(Color a, Color b) => a.Equals(b);
        public static bool operator !=(Color a, Color b) => !a.Equals(b);

        public bool Equals(Color color) => r == color.r && g == color.g && b == color.b && a == color.a;
        public override bool Equals(object obj) => obj is Color && Equals((Color)obj);
        public override int GetHashCode() => r.GetHashCode() ^ g.GetHashCode() ^ b.GetHashCode() ^ a.GetHashCode();

        public override string ToString() => string.Format(CultureInfo.InvariantCulture, "{{R:{0} G:{1} B:{2} A:{3}}}", new object[] { r, g, b, a });

        public static Color ReadBgra4444(byte[] data, int index)
        {
            int pixel = data[index + 0];
            byte b = (byte)((pixel << 4) & 0xf0);
            byte g = (byte)(pixel & 0xf0);
            pixel = data[index + 1];
            byte r = (byte)((pixel << 4) & 0xf0);
            byte a = (byte)(pixel & 0xf0);

            return new Color(r, g, b, a);
        }

        public static void WriteBgra4444(Color color, byte[] data, int index)
        {
            data[index + 0] = (byte)((color.b >> 4) | (color.g & 0xf0));
            data[index + 1] = (byte)((color.r >> 4) | (color.a & 0xf0));
        }

        public static Color ReadBgrx5551(byte[] data, int index)
        {
            int pixel = data[index + 0] | (data[index + 1] << 8);
            byte b = (byte)((pixel << 3) & 0xf8);
            byte g = (byte)((pixel >> 2) & 0xf8);
            byte r = (byte)((pixel >> 7) & 0xf8);

            return new Color(r, g, b);
        }

        public static void WriteBgrx5551(Color color, byte[] data, int index)
        {
            data[index + 0] = (byte)((color.b >> 3) | ((color.g & 0x38) << 2));
            data[index + 1] = (byte)((color.g >> 6) | ((color.r & 0xf8) >> 1) | 0x80);
        }

        public static Color ReadBgra5551(byte[] data, int index)
        {
            int pixel = data[index + 0] | (data[index + 1] << 8);
            byte b = (byte)((pixel << 3) & 0xf8);
            byte g = (byte)((pixel >> 2) & 0xf8);
            byte r = (byte)((pixel >> 7) & 0xf8);
            byte a = (byte)((pixel >> 15) * 255);

            return new Color(r, g, b, a);
        }

        public static void WriteBgra5551(Color color, byte[] data, int index)
        {
            data[index + 0] = (byte)((color.b >> 3) | ((color.g & 0x38) << 2));
            data[index + 1] = (byte)((color.g >> 6) | ((color.r & 0xf8) >> 1) | (color.a & 0x80));
        }

        public static Color ReadBgr565(byte[] data, int index)
        {
            int pixel = data[index + 0] | (data[index + 1] << 8);
            byte b = (byte)((pixel << 3) & 0xf8);
            byte g = (byte)((pixel >> 3) & 0xfc);
            byte r = (byte)((pixel >> 8) & 0xf8);

            return new Color(r, g, b);
        }

        public static void WriteBgr565(Color color, byte[] data, int index)
        {
            data[index + 0] = (byte)((color.b >> 3) | ((color.g & 0x1C) << 3));
            data[index + 1] = (byte)((color.g >> 5) | (color.r & 0xf8));
        }

        public static Color ReadBgrx(byte[] data, int index)
        {
            byte b = data[index + 0];
            byte g = data[index + 1];
            byte r = data[index + 2];

            return new Color(r, g, b);
        }

        public static void WriteBgrx(Color color, byte[] data, int index)
        {
            data[index + 0] = color.b;
            data[index + 1] = color.g;
            data[index + 2] = color.r;
            data[index + 3] = 255;
        }

        public static Color ReadBgra(byte[] data, int index)
        {
            byte b = data[index + 0];
            byte g = data[index + 1];
            byte r = data[index + 2];
            byte a = data[index + 3];

            return new Color(r, g, b, a);
        }

        public static void WriteBgra(Color color, byte[] data, int index)
        {
            data[index + 0] = color.b;
            data[index + 1] = color.g;
            data[index + 2] = color.r;
            data[index + 3] = color.a;
        }

        public static Color ReadRgbx(byte[] data, int index)
        {
            byte r = data[index + 0];
            byte g = data[index + 1];
            byte b = data[index + 2];

            return new Color(r, g, b);
        }

        public static void WriteRgbx(Color color, byte[] data, int index)
        {
            data[index + 0] = color.r;
            data[index + 1] = color.g;
            data[index + 2] = color.b;
            data[index + 3] = 255;
        }

        public static Color ReadRgba(byte[] data, int index)
        {
            byte r = data[index + 0];
            byte g = data[index + 1];
            byte b = data[index + 2];
            byte a = data[index + 3];

            return new Color(r, g, b, a);
        }

        public static void WriteRgba(Color color, byte[] data, int index)
        {
            data[index + 0] = color.r;
            data[index + 1] = color.g;
            data[index + 2] = color.b;
            data[index + 3] = color.a;
        }

        public static Color Lerp(Color x, Color y, float amount)
        {
            byte r = (byte)MathHelper.Lerp(x.r, y.r, amount);
            byte g = (byte)MathHelper.Lerp(x.g, y.g, amount);
            byte b = (byte)MathHelper.Lerp(x.b, y.b, amount);
            byte a = (byte)MathHelper.Lerp(x.a, y.a, amount);

            return new Color(r, g, b, a);
        }

        private static readonly Color black = new Color(0, 0, 0, 255);
        private static readonly Color white = new Color(255, 255, 255, 255);
        private static readonly Color transparent = new Color(0, 0, 0, 0);

        public static Color White => white;
        public static Color Black => black;
        public static Color Transparent => transparent;

        public static bool TryParse(string s, out Color color)
        {
            color = new Color();
            color.a = 255;

            if (string.IsNullOrEmpty(s))
                return false;

            var tokens = s.Split(new char[0], StringSplitOptions.RemoveEmptyEntries);

            if (tokens.Length < 3 || tokens.Length > 4)
                return false;

            if (!byte.TryParse(tokens[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out color.r))
                return false;

            if (!byte.TryParse(tokens[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out color.g))
                return false;

            if (!byte.TryParse(tokens[2], NumberStyles.Integer, CultureInfo.InvariantCulture, out color.b))
                return false;

            if (tokens.Length > 3 && !byte.TryParse(tokens[3], NumberStyles.Integer, CultureInfo.InvariantCulture, out color.a))
                return false;

            return true;
        }

        public static Color Parse(string s)
        {
            Color c;

            if (!TryParse(s, out c))
                throw new FormatException(string.Format("'{0}' is not a color", s));

            return c;
        }
    }
}
