using System;
using System.Collections;
using System.Collections.Generic;
using System.Drawing;
using OrComp = Orienteering.Competition;
using Cairo;

namespace Orienteering.GUI {
    public abstract class Object {
        internal Orienteering.Competition.Object data;
        public double X {
            get { return data.X; }
            set { data.X = value; }
        }
        public double Y {
            get { return data.Y; }
            set { data.Y = value; }
        }
        public int Number {
            get { return data.Number; }
            set { data.Number = value; }
        }

        public Object() {
        }

        public Object (Orienteering.Competition.Object _data) {
            data = _data;
        }

        public Object (double _x, double _y, int _number) {
            X=_x; Y=_y;
            Number=_number;
        }

        public abstract void Draw(System.Drawing.Graphics gr, double pos); 
        public abstract void Draw(Cairo.Context gr, double pos); 

        private double ComputeOffset(double ang, double w, double h) {
            double z = Config.NumberOffset+Config.Radius, tmp = z;
            double k = z*10+47, m;

            for (int i=0; i<30; i++) {
                m = (z+k)/2;
                PointF[] body = new PointF[4];
                PointF corner =
                    new PointF( (float)(X+Math.Sin(ang)*m+w/2), (float)(Y+Math.Cos(ang)*m+h/2) );
                double d = m;
                for (int j=0; j<4; j++) {
                    body[j] = corner;
                    if (j%2 == 1) body[j].X -= (float)w;
                    if (j/2 == 1) body[j].Y -= (float)h;

                    d = Math.Min(d, Standard.hypot(body[j].X-X, body[j].Y-Y));
                }

                if (corner.X>X+0.1 && corner.X-w<X-0.1) 
                    d = Math.Min(d, Math.Min(Math.Abs(Y-corner.Y), Math.Abs(Y-corner.Y+h)) );
                if (corner.Y>Y+0.1 && corner.Y-h<Y-0.1) 
                    d = Math.Min(d, Math.Min(Math.Abs(X-corner.X), Math.Abs(X-corner.X+w) ));

                if (d<tmp) z=m;
                else k=m; 
            }
            return z;
        }

        public void DrawNumber(System.Drawing.Graphics gr, int num, double ang) {
            SizeF size = gr.MeasureString(Convert.ToString(num), Config.Font);
            double offset = ComputeOffset(ang, size.Width, size.Height);

            double[] a = new double[2] {
                X+Math.Sin(ang)*offset-size.Width/2, Y+Math.Cos(ang)*offset-size.Height/2
            };
            gr.DrawString(Convert.ToString(num), Config.Font,
                    Config.Brush, new PointF( (float)a[0], (float)a[1]) );
        }

        public void DrawNumber(Cairo.Context gr, int num, double ang) {
            gr.SetFontSize(Config.FontSize);
            TextExtents size = gr.TextExtents(Convert.ToString(num));
            double offset = ComputeOffset(ang, size.Width, size.Height);

            double[] a = new double[2] {
                X+Math.Sin(ang)*offset-size.Width/2, Y+Math.Cos(ang)*offset+size.Height/2
            };
            gr.MoveTo(a[0], a[1]);
            gr.ShowText(Convert.ToString(num));

        }
    }

    public class Control : Object {
        public Control (double _x, double _y, int _number, Stage _stage) {
            data = new OrComp.Control(_x, _y, _number, _stage.data);
        }

        public Control(Orienteering.Competition.Control data) : base(data) {
        }

        public override void Draw(System.Drawing.Graphics gr, double pos) {
            RectangleF rec = new RectangleF((float)(X-Config.Radius),
                    (float)(Y-Config.Radius),
                    (float)(2*Config.Radius),
                    (float)(2*Config.Radius));
            gr.DrawEllipse(Config.Pen, rec);
        }

        public override void Draw (Cairo.Context gr, double pos) {
            gr.MoveTo (X+Config.Radius, Y);
            gr.Arc (X, Y, Config.Radius, 0, 2*Math.PI);
        }

    }

    public class Start : Object {
        public Start(double _x, double _y, int _number, Stage _stage) {
            data = new OrComp.Start(_x, _y, _number, _stage.data);
        }
        public Start(Orienteering.Competition.Start data) : base(data) {
        }
        public override void Draw (System.Drawing.Graphics gr, double pos) {
            PointF []body = new PointF[3];
            for (int i=0; i<3; i++) {
                double uhol = i*2*Math.PI/3+pos;
                double dlzka = 2*Config.Start/Math.Sqrt(3);
                body[i] = new PointF((float)(X+Math.Sin(uhol)*dlzka), (float)(Y+Math.Cos(uhol)*dlzka));
            }

            gr.DrawPolygon(Config.Pen, body);
        }

        public override void Draw (Cairo.Context gr, double pos) {
            PointD []body = new PointD[3];
            for (int i=0; i<3; i++) {
                double uhol = i*2*Math.PI/3+pos;
                double dlzka = 2*Config.Start/Math.Sqrt(3);
                body[i] = new PointD(X+Math.Sin(uhol)*dlzka, Y+Math.Cos(uhol)*dlzka);
            }

            gr.MoveTo(body[2]);
            for (int i=0; i<3; i++)
                gr.LineTo(body[i]);
            gr.LineTo(body[0]);
            gr.ClosePath();
        }
    }

    public class Finish : Object {
        public Finish (double _x, double _y, int _number, Stage _stage) {
            data = new OrComp.Finish(_x, _y, _number, _stage.data);
        }
        public Finish(Orienteering.Competition.Finish data) : base(data) {
        }
        public override void Draw (System.Drawing.Graphics gr, double pos) {
            RectangleF rec = new RectangleF((float)(X-Config.FinishRadius),
                    (float)(Y-Config.FinishRadius),
                    (float)(2*Config.FinishRadius),
                    (float)(2*Config.FinishRadius));
            gr.DrawEllipse(Config.Pen, rec);

            rec = new RectangleF((float)(X-Config.Start),
                    (float)(Y-Config.Start),
                    (float)(2*Config.Start),
                    (float)(2*Config.Start));
            gr.DrawEllipse(Config.Pen, rec);
        }

        public override void Draw (Cairo.Context gr, double pos) {
            gr.MoveTo (X+Config.FinishRadius, Y);
            gr.Arc (X, Y, Config.FinishRadius, 0, 2*Math.PI);

            gr.MoveTo (X+Config.Start, Y);
            gr.Arc (X, Y, Config.Start, 0, 2*Math.PI);
        }
    }

    public class Course {
        private Orienteering.Competition.Course data;

        public Course(Orienteering.Competition.Course _data) {
            data = _data;
        }

        public List<Object> Objects {
            get {
                List<Orienteering.Competition.Object> zoz = data.Objects;
                List<Object> list = new List<Object>();
                foreach (Orienteering.Competition.Object it in zoz) {
                    if (it is Orienteering.Competition.Start)
                        list.Add(new Start((Orienteering.Competition.Start)it));
                    if (it is Orienteering.Competition.Control)
                        list.Add(new Control((Orienteering.Competition.Control)it));
                    if (it is Orienteering.Competition.Finish)
                        list.Add(new Finish((Orienteering.Competition.Finish)it));
                }
                return list;
            }
        }

        public void AddObject(Object x) {
            data.AddObject(x.data);
        }

        private static double NumberAngle(double a, double b) {
            if (a > b) {
                double g = a;
                a = b;
                b = g;
            }
            if (b-a>Math.PI) return (a+b)/2;
            else return (a+b)/2+Math.PI;
        }

        public void Draw(Cairo.Context gr) {
            gr.Color = Config.CourseColor;
            gr.LineWidth = Config.LineWidth;
            List<Object> zoz = Objects;

            for (int i=0; i<zoz.Count; i++) {
                double posun=0;
                if (i+1<zoz.Count) {
                    posun = Math.Atan2(zoz[i+1].X-zoz[i].X, zoz[i+1].Y-zoz[i].Y);
                    double odstup = Config.Offset;
                    gr.MoveTo(new PointD(zoz[i].X+odstup*Math.Sin(posun),
                                zoz[i].Y+odstup*Math.Cos(posun)));
                    if (2*odstup < Standard.hypot(zoz[i].X-zoz[i+1].X, zoz[i].Y-zoz[i+1].Y))
                        gr.LineTo(new PointD(zoz[i+1].X-odstup*Math.Sin(posun),
                                    zoz[i+1].Y-odstup*Math.Cos(posun)));
                }
                if (zoz[i] is Control) {
                    double uhol = posun+Math.PI/2;
                    if (i>=1)
                        uhol = NumberAngle(posun,
                                Math.Atan2(zoz[i-1].X-zoz[i].X, zoz[i-1].Y-zoz[i].Y));
                    zoz[i].DrawNumber(gr, i, uhol);
                }
                zoz[i].Draw(gr, posun);
            }
            gr.Stroke();
        }

        public void Draw(System.Drawing.Graphics gr) {
            List<Object> zoz = Objects;
            for (int i=0; i<zoz.Count; i++) {
                double posun=0;
                if (i+1<zoz.Count) {
                    posun = Math.Atan2(zoz[i+1].X-zoz[i].X, zoz[i+1].Y-zoz[i].Y);
                    double odstup = Config.Offset;
                    if (2*odstup < Standard.hypot(zoz[i].X-zoz[i+1].X, zoz[i].Y-zoz[i+1].Y))
                        gr.DrawLine(Config.Pen,
                            new PointF((float)(zoz[i].X+odstup*Math.Sin(posun)),
                                (float)(zoz[i].Y+odstup*Math.Cos(posun))),
                            new PointF((float)(zoz[i+1].X-odstup*Math.Sin(posun)),
                                (float)(zoz[i+1].Y-odstup*Math.Cos(posun))));
                }
                if (zoz[i] is Control) {
                    double uhol = posun+Math.PI/2;
                    if (i>=1)
                        uhol = NumberAngle(posun,
                                Math.Atan2(zoz[i-1].X-zoz[i].X, zoz[i-1].Y-zoz[i].Y));
                    zoz[i].DrawNumber(gr, i, uhol);
                }
                zoz[i].Draw(gr, posun);
            }
        }

        public void Export(string path, ExportFormat format, int w, int h) {
            //Nefunguje s oficialnym Monom, lebo Mono nema funkcne prepojenie na Cairo 1.2
            Surface surf = null;
            switch (format) {
                /*
                case ExportFormat.PS:
                    surf = new Cairo.PSSurface(path, w, h);
                break;
                case ExportFormat.PDF:
                    surf = new Cairo.PdfSurface(path, w, h);
                break;
                case ExportFormat.SVG:
                    surf = new Cairo.SvgSurface(path, w, h);
                break;
                */
                case ExportFormat.PNG:
                    surf = new Cairo.ImageSurface(Format.Argb32, w, h);
                break;
            }
            Context gr = new Context(surf);

            Draw(gr);
            gr.ShowPage();
            if (format == ExportFormat.PNG)
                surf.WriteToPng(path);
            surf.Finish();
        }

        public Course(string _name, Stage _stage) {
            data = new Orienteering.Competition.Course(_name, _stage.data);
        }
    }
}
