use cairo::Format;
use cairo::{ImageSurface, SvgSurface};
use geo::{polygon, LineString, MultiLineString, MultiPolygon, Polygon};
use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};

use crate::context::command_line_options::Opt;
use crate::context::palette::Palette;
use crate::context::renderer::{PngRenderContainer, RenderContainer, SvgRenderContainer};
use iso8601::Date::*;

pub mod command_line_options;
pub mod palette;
pub mod renderer;

pub struct Context {
    render_container: Box<dyn RenderContainer>,
    pub height: f64,
    pub width: f64,
    pub line_size: f64,
    pub pseudo_random_number_generator: Box<StdRng>,
    pub seed: u64,
}

impl Context {
    /// generate a frame for the image
    pub fn frame(&mut self, left: f64, bottom: f64, right: f64, top: f64) -> Polygon<f64> {
        polygon![
        (x: left, y: top),
        (x: self.width - right, y: top),
        (x: self.width - right, y: self.height - bottom),
        (x: left , y: self.height - bottom),
        ]
    }

    /// a fall back operation, if you need a function not provided by this struct.
    pub fn cairo_context(&self) -> &cairo::Context {
        self.render_container.context()
    }

    pub fn draw_multipolygon(&self, multi_polygon: &MultiPolygon<f64>) {
        for polygon in multi_polygon.iter() {
            self.draw_polygon(polygon);
        }
    }

    pub fn draw_polygon(&self, polygon: &Polygon<f64>) {
        self.draw_line_string(polygon.exterior());
        for interior in polygon.interiors() {
            self.draw_line_string(interior);
        }
    }

    pub fn draw_line_string(&self, line_string: &LineString<f64>) {
        let context = self.render_container.context();
        let mut init = true;
        for point in line_string.clone().into_points() {
            if init {
                context.move_to(point.x(), point.y());
                init = false;
            } else {
                context.line_to(point.x(), point.y());
            }
        }
        context.stroke();
    }

    pub fn draw_line_strings(&self, line_strings: &Vec<LineString<f64>>) {
        for lines in line_strings {
            self.draw_line_string(&lines);
        }
    }

    pub fn draw_multiline_string(&self, multilinestring: &MultiLineString<f64>) {
        for line in multilinestring.iter() {
            self.draw_line_string(line);
        }
    }

    /// restore state which was saved before by the context
    pub fn restore(&self) {
        self.render_container.context().restore();
    }

    /// save state to restore later again
    pub fn save(&self) {
        self.render_container.context().save();
    }
    /// scale, but line with stays
    pub fn scale(&self, factor: f64) {
        let context = self.render_container.context();
        context.scale(factor, factor);
        context.set_line_width(1. / factor);
    }

    /// translate to the center of the image
    pub fn translate_center(&self) {
        self.translate(self.width / 2., self.height / 2.);
    }

    /// translate to a position
    pub fn translate(&self, tx: f64, ty: f64) {
        let context = self.render_container.context();
        context.translate(tx, ty);
    }

    pub fn render(self) {
        self.render_container.render();
    }

    pub fn gen_rng(&mut self) -> StdRng {
        StdRng::seed_from_u64(self.pseudo_random_number_generator.gen())
    }

    pub fn create() -> Context {
        let opt = Opt::get_command_line_arguments();
        let line_size = opt.line_size.clone();

        // todo : this is not correct
        let mm = 2.8346456693; // 1mm = 2.8346456693pt
        let (mut height, mut width) = match opt.format {
            None => (f64::from(opt.height.clone()), f64::from(opt.width.clone())),
            Some(format) => match &format[..] {
                "a3" => (297. * mm, 420. * mm),
                "a4" => (210. * mm, 297. * mm),
                "a5" => (148. * mm, 210. * mm),
                "x220" => (768., 1366.),
                _ => (f64::from(opt.height.clone()), f64::from(opt.width.clone())),
            },
        };

        print!("height: {}\n", height);
        print!("width: {}\n", width);

        if opt.orientation == "portrait" {
            let cache = height;
            height = width;
            width = cache;
        }

        let random_seed = match opt.random_seed {
            Some(random_seed) => random_seed,
            None => match opt.random_seed_date {
                Some(YMD { year, month, day }) => {
                    ((10000 + year) as u32 * 365 + month * 12 + day) as u64
                }
                Some(Week { year, ww, d }) => ((10000 + year) as u32 * 365 + ww * 7 + d) as u64,
                Some(Ordinal { year, ddd }) => ((10000 + year) as u32 * 365 + ddd) as u64,
                None => rand::random(),
            },
        };

        println!("random seed {}", random_seed);
        let mut rng = StdRng::seed_from_u64(random_seed);

        // color
        let main_color = Palette::random_color(StdRng::seed_from_u64(rng.gen()));
        let palette = match &opt.color_type[..] {
            "bright_on_dark" => Palette::bright_on_dark(main_color),
            "dark_on_bright" => Palette::dark_on_bright(main_color),
            _ => Palette::bright_on_dark(main_color),
        };

        let render_container: Box<dyn RenderContainer> = if opt.output_type == "svg" {
            let svg_surface = SvgSurface::new(
                f64::from(width),
                f64::from(height),
                Some(opt.output.as_path()),
            )
            .expect("Can't svg surface");
            Box::new(SvgRenderContainer::new(svg_surface, palette)) as Box<dyn RenderContainer>
        } else if opt.output_type == "png" {
            let png_surface = ImageSurface::create(Format::Rgb24, width as i32, height as i32)
                .expect("Can't create png surface");
            Box::new(PngRenderContainer::new(
                opt.output.to_string_lossy().to_string(),
                png_surface,
                palette,
            )) as Box<dyn RenderContainer>
        } else {
            panic!("output type unknown");
        };

        render_container.init(width as f64, height as f64, line_size);

        Context {
            height,
            width,
            line_size,
            pseudo_random_number_generator: Box::new(rng),
            render_container,
            seed: random_seed,
        }
    }
}