// This file is part of ICU4X. For terms of use, please see the file
// called LICENSE at the top level of the ICU4X source tree
// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).

use std::{fs::File, path::PathBuf};

use askama::Template;
use icu::datetime::fieldsets::builder::*;

const GENERATED_BY: &str = std::concat!("@generated by ", std::file!());

#[derive(Debug)]
struct ConsumedOptions {
    length: bool,
    alignment: bool,
    year_style: bool,
}

impl ConsumedOptions {
    fn from_builder(builder: FieldSetBuilder) -> Option<Self> {
        match builder.build_composite() {
            Ok(_) => Some(ConsumedOptions {
                length: true,
                alignment: true,
                year_style: true,
            }),
            Err(BuilderError::SuperfluousOptions(options)) => Some(ConsumedOptions {
                length: options.length.is_none(),
                alignment: options.alignment.is_none(),
                year_style: options.year_style.is_none(),
            }),
            Err(BuilderError::InvalidDateFields) => None,
            Err(e) => panic!("unexpected error: {e}"),
        }
    }
}

#[derive(Debug, Copy, Clone)]
enum ConstructorType {
    CompiledData,
    WithProvider,
}

impl ConstructorType {
    pub const VALUES: &'static [Self] = &[Self::CompiledData, Self::WithProvider];
    pub fn is_with_provider(self) -> bool {
        matches!(self, Self::WithProvider)
    }
    pub fn prefix(self) -> &'static str {
        match self {
            Self::CompiledData => "include_",
            Self::WithProvider => "load_",
        }
    }
    pub fn suffix_ffi(self) -> &'static str {
        match self {
            Self::CompiledData => "",
            Self::WithProvider => "_with_provider",
        }
    }
    pub fn suffix_rust(self) -> &'static str {
        match self {
            Self::CompiledData => "",
            Self::WithProvider => "_with_buffer_provider",
        }
    }
    pub fn cargo_feature(self) -> &'static str {
        match self {
            Self::CompiledData => "compiled_data",
            Self::WithProvider => "buffer_provider",
        }
    }
}

#[derive(Copy, Clone)]
enum FormatterFlavor {
    Date,
    Time,
    Zone,
    #[allow(dead_code)] // temporary
    DateTime,
}

impl FormatterFlavor {
    pub fn camel(self) -> &'static str {
        match self {
            FormatterFlavor::Date => "Date",
            FormatterFlavor::Time => "Time",
            FormatterFlavor::Zone => "Zone",
            FormatterFlavor::DateTime => "DateTime",
        }
    }
    pub fn lower(self) -> &'static str {
        match self {
            FormatterFlavor::Date => "date",
            FormatterFlavor::Time => "time",
            FormatterFlavor::Zone => "zone",
            FormatterFlavor::DateTime => "datetime",
        }
    }
    pub fn formatter_kinds(self) -> &'static [FormatterKind] {
        match self {
            FormatterFlavor::Date | FormatterFlavor::DateTime => &[
                FormatterKind {
                    is_fixed_calendar: false,
                    is_gregorian: false,
                },
                FormatterKind {
                    is_fixed_calendar: true,
                    is_gregorian: true,
                },
            ],
            FormatterFlavor::Time | FormatterFlavor::Zone => &[FormatterKind {
                is_fixed_calendar: true,
                is_gregorian: false,
            }],
        }
    }
    pub fn field_set(self) -> &'static str {
        match self {
            FormatterFlavor::Date => "DateFieldSet",
            FormatterFlavor::Time => "TimeFieldSet",
            FormatterFlavor::Zone => "ZoneFieldSet",
            FormatterFlavor::DateTime => "CompositeDateTimeFieldSet",
        }
    }
    pub fn has_date(self) -> bool {
        matches!(self, FormatterFlavor::Date | FormatterFlavor::DateTime)
    }
    pub fn has_time(self) -> bool {
        matches!(self, FormatterFlavor::Time | FormatterFlavor::DateTime)
    }
    pub fn is_time_only(self) -> bool {
        matches!(self, FormatterFlavor::Time)
    }
    pub fn is_zone_only(self) -> bool {
        matches!(self, FormatterFlavor::Zone)
    }
}

#[derive(Copy, Clone)]
struct FormatterKind {
    pub is_fixed_calendar: bool,
    pub is_gregorian: bool,
}

impl FormatterKind {
    pub fn rust_type(self) -> &'static str {
        match self.is_fixed_calendar {
            true => "FixedCalendarDateTimeFormatter",
            false => "DateTimeFormatter",
        }
    }
    pub fn rustlink(self) -> &'static str {
        match (self.is_fixed_calendar, self.is_gregorian) {
            (true, true) => "FixedCalendarDateTimeFormatter",
            (true, false) => "NoCalendarFormatter",
            (false, _) => "DateTimeFormatter",
        }
    }
    pub fn rustlink_doctype(self) -> &'static str {
        match (self.is_fixed_calendar, self.is_gregorian) {
            (true, true) => "Struct",
            (true, false) => "Typedef",
            (false, _) => "Struct",
        }
    }
    pub fn rustlink_doctype_fn(self) -> &'static str {
        match (self.is_fixed_calendar, self.is_gregorian) {
            (true, true) => "FnInStruct",
            (true, false) => "FnInTypedef",
            (false, _) => "FnInStruct",
        }
    }
}

#[derive(Template)]
#[template(path = "datetime_formatter.rs.jinja")]
struct DateTimeFormatterTemplate {
    flavor: FormatterFlavor,
    variants: Vec<DateTimeFormatterVariant>,
}

struct DateTimeFormatterVariant {
    inner: DateTimeFormatterVariantInner,
    consumed_options: ConsumedOptions,
}

enum DateTimeFormatterVariantInner {
    Date(DateFields),
    Time,
    #[allow(dead_code)] // temporary
    DateTime(DateFields),
}

impl DateTimeFormatterVariant {
    pub fn name_upper(&self) -> &'static str {
        use DateTimeFormatterVariantInner as Inner;
        match self.inner {
            Inner::Date(DateFields::D) => "D",
            Inner::Date(DateFields::MD) => "MD",
            Inner::Date(DateFields::YMD) => "YMD",
            Inner::Date(DateFields::DE) => "DE",
            Inner::Date(DateFields::MDE) => "MDE",
            Inner::Date(DateFields::YMDE) => "YMDE",
            Inner::Date(DateFields::E) => "E",
            Inner::Date(DateFields::M) => "M",
            Inner::Date(DateFields::YM) => "YM",
            Inner::Date(DateFields::Y) => "Y",
            Inner::Time => "T",
            Inner::DateTime(DateFields::D) => "DT",
            Inner::DateTime(DateFields::MD) => "MDT",
            Inner::DateTime(DateFields::YMD) => "YMDT",
            Inner::DateTime(DateFields::DE) => "DET",
            Inner::DateTime(DateFields::MDE) => "MDET",
            Inner::DateTime(DateFields::YMDE) => "YMDET",
            Inner::DateTime(DateFields::E) => "ET",
            _ => unreachable!("unknown variant"),
        }
    }
    pub fn name_lower(&self) -> &'static str {
        use DateTimeFormatterVariantInner as Inner;
        match self.inner {
            Inner::Date(DateFields::D) => "d",
            Inner::Date(DateFields::MD) => "md",
            Inner::Date(DateFields::YMD) => "ymd",
            Inner::Date(DateFields::DE) => "de",
            Inner::Date(DateFields::MDE) => "mde",
            Inner::Date(DateFields::YMDE) => "ymde",
            Inner::Date(DateFields::E) => "e",
            Inner::Date(DateFields::M) => "m",
            Inner::Date(DateFields::YM) => "ym",
            Inner::Date(DateFields::Y) => "y",
            Inner::Time => "t",
            Inner::DateTime(DateFields::D) => "dt",
            Inner::DateTime(DateFields::MD) => "mdt",
            Inner::DateTime(DateFields::YMD) => "ymdt",
            Inner::DateTime(DateFields::DE) => "det",
            Inner::DateTime(DateFields::MDE) => "mdet",
            Inner::DateTime(DateFields::YMDE) => "ymdet",
            Inner::DateTime(DateFields::E) => "et",
            _ => unreachable!("unknown variant"),
        }
    }
    pub fn supports_zone(&self) -> bool {
        use DateTimeFormatterVariantInner as Inner;
        let date_fields = match self.inner {
            Inner::Date(date_fields) => date_fields,
            Inner::Time => return true,
            Inner::DateTime(date_fields) => date_fields,
        };
        !date_fields.is_calendar_period()
    }
    pub fn supports_time(&self) -> bool {
        use DateTimeFormatterVariantInner as Inner;
        let date_fields = match self.inner {
            Inner::Date(date_fields) => date_fields,
            Inner::Time => return true,
            Inner::DateTime(_) => return false, // it already has a time
        };
        !date_fields.is_calendar_period()
    }
    pub fn is_demo_constructor(&self) -> bool {
        use DateTimeFormatterVariantInner as Inner;
        matches!(
            self.inner,
            Inner::Date(DateFields::YMD) | Inner::Time | Inner::DateTime(DateFields::YMD)
        )
    }
    pub fn is_only_constructor(&self) -> bool {
        use DateTimeFormatterVariantInner as Inner;
        matches!(self.inner, Inner::Time)
    }
}

#[derive(Template)]
#[template(path = "zoned_formatter.rs.jinja")]
struct ZonedFormatterTemplate {
    flavor: FormatterFlavor,
    consumed_options: Option<ConsumedOptions>,
    variants: Vec<ZonedFormatterVariant>,
}

struct ZonedFormatterVariant {
    zone_style: ZoneStyle,
}

impl ZonedFormatterVariant {
    pub fn name_lower(&self) -> &str {
        match self.zone_style {
            ZoneStyle::SpecificLong => "specific_long",
            ZoneStyle::SpecificShort => "specific_short",
            ZoneStyle::LocalizedOffsetLong => "localized_offset_long",
            ZoneStyle::LocalizedOffsetShort => "localized_offset_short",
            ZoneStyle::GenericLong => "generic_long",
            ZoneStyle::GenericShort => "generic_short",
            ZoneStyle::Location => "location",
            ZoneStyle::ExemplarCity => "exemplar_city",
            _ => unreachable!("unknown variant"),
        }
    }
    pub fn name_camel(&self) -> &str {
        match self.zone_style {
            ZoneStyle::SpecificLong => "SpecificLong",
            ZoneStyle::SpecificShort => "SpecificShort",
            ZoneStyle::LocalizedOffsetLong => "LocalizedOffsetLong",
            ZoneStyle::LocalizedOffsetShort => "LocalizedOffsetShort",
            ZoneStyle::GenericLong => "GenericLong",
            ZoneStyle::GenericShort => "GenericShort",
            ZoneStyle::Location => "Location",
            ZoneStyle::ExemplarCity => "ExemplarCity",
            _ => unreachable!("unknown variant"),
        }
    }
    pub fn load_fn(&self) -> &str {
        match self.zone_style {
            ZoneStyle::SpecificLong => "specific_long_names_with_fallback",
            ZoneStyle::SpecificShort => "specific_short_names_with_fallback",
            ZoneStyle::LocalizedOffsetLong => "localized_offset_names_with_fallback",
            ZoneStyle::LocalizedOffsetShort => "localized_offset_names_with_fallback",
            ZoneStyle::GenericLong => "generic_long_names_with_fallback",
            ZoneStyle::GenericShort => "generic_short_names_with_fallback",
            ZoneStyle::Location => "location_names",
            ZoneStyle::ExemplarCity => "exemplar_city_names",
            _ => unreachable!("unknown variant"),
        }
    }
    pub fn is_demo_constructor(&self) -> bool {
        matches!(self.zone_style, ZoneStyle::SpecificLong)
    }
}

pub fn main() {
    let mut date_formatter_template = DateTimeFormatterTemplate {
        flavor: FormatterFlavor::Date,
        variants: Vec::new(),
    };
    let mut time_formatter_template = DateTimeFormatterTemplate {
        flavor: FormatterFlavor::Time,
        variants: Vec::new(),
    };
    let mut datetime_formatter_template = DateTimeFormatterTemplate {
        flavor: FormatterFlavor::DateTime,
        variants: Vec::new(),
    };
    let mut zone_formatter_template = ZonedFormatterTemplate {
        flavor: FormatterFlavor::Zone,
        consumed_options: None,
        variants: Vec::new(),
    };
    let mut zoned_date_formatter_template = ZonedFormatterTemplate {
        flavor: FormatterFlavor::Date,
        consumed_options: None,
        variants: Vec::new(),
    };
    let mut zoned_time_formatter_template = ZonedFormatterTemplate {
        flavor: FormatterFlavor::Time,
        consumed_options: None,
        variants: Vec::new(),
    };
    let mut zoned_datetime_formatter_template = ZonedFormatterTemplate {
        flavor: FormatterFlavor::DateTime,
        consumed_options: None,
        variants: Vec::new(),
    };

    for date_fields in DateFields::VALUES.iter() {
        // Determine the options for these date fields
        let mut builder = FieldSetBuilder::new();
        builder.date_fields = Some(*date_fields);
        builder.length = Some(Default::default());
        builder.alignment = Some(Default::default());
        builder.year_style = Some(Default::default());

        let consumed_options = ConsumedOptions::from_builder(builder.clone()).unwrap();
        assert!(consumed_options.length); // all constructors accept a length
        date_formatter_template
            .variants
            .push(DateTimeFormatterVariant {
                inner: DateTimeFormatterVariantInner::Date(*date_fields),
                consumed_options,
            });

        builder.time_precision = Some(Default::default());
        let Some(consumed_options) = ConsumedOptions::from_builder(builder.clone()) else {
            // calendar period field set
            continue;
        };
        assert!(consumed_options.length); // all constructors accept a length
        datetime_formatter_template
            .variants
            .push(DateTimeFormatterVariant {
                inner: DateTimeFormatterVariantInner::DateTime(*date_fields),
                consumed_options,
            });
    }

    for zone_style in ZoneStyle::VALUES.iter() {
        zone_formatter_template
            .variants
            .push(ZonedFormatterVariant {
                zone_style: *zone_style,
            });
        zoned_date_formatter_template
            .variants
            .push(ZonedFormatterVariant {
                zone_style: *zone_style,
            });
        zoned_time_formatter_template
            .variants
            .push(ZonedFormatterVariant {
                zone_style: *zone_style,
            });
        zoned_datetime_formatter_template
            .variants
            .push(ZonedFormatterVariant {
                zone_style: *zone_style,
            });
    }

    {
        let mut builder = FieldSetBuilder::new();
        builder.time_precision = Some(Default::default());
        builder.length = Some(Default::default());
        builder.alignment = Some(Default::default());
        builder.year_style = Some(Default::default());
        let consumed_options = ConsumedOptions::from_builder(builder.clone()).unwrap();
        time_formatter_template
            .variants
            .push(DateTimeFormatterVariant {
                inner: DateTimeFormatterVariantInner::Time,
                consumed_options,
            });

        builder.zone_style = Some(ZoneStyle::LocalizedOffsetShort);
        let consumed_options = ConsumedOptions::from_builder(builder.clone()).unwrap();
        assert!(!consumed_options.year_style); // template doesn't handle year style
        zoned_time_formatter_template.consumed_options = Some(consumed_options);

        builder.time_precision = None;
        let consumed_options = ConsumedOptions::from_builder(builder.clone()).unwrap();
        assert!(!consumed_options.year_style); // template doesn't handle year style
        zone_formatter_template.consumed_options = Some(consumed_options);
    }

    let mut path_buf = PathBuf::new();
    path_buf.push(env!("CARGO_MANIFEST_DIR"));
    path_buf.push("../../../ffi/capi/src");

    {
        let mut path_buf = path_buf.clone();
        path_buf.push("date_formatter.rs");
        let mut file = File::create(&path_buf).unwrap();
        use std::io::Write;
        writeln!(&mut file, "{date_formatter_template}").unwrap();
    }

    {
        let mut path_buf = path_buf.clone();
        path_buf.push("time_formatter.rs");
        let mut file = File::create(&path_buf).unwrap();
        use std::io::Write;
        writeln!(&mut file, "{time_formatter_template}").unwrap();
    }

    {
        let mut path_buf = path_buf.clone();
        path_buf.push("date_time_formatter.rs");
        let mut file = File::create(&path_buf).unwrap();
        use std::io::Write;
        writeln!(&mut file, "{datetime_formatter_template}").unwrap();
    }

    {
        let mut path_buf = path_buf.clone();
        path_buf.push("timezone_formatter.rs");
        let mut file = File::create(&path_buf).unwrap();
        use std::io::Write;
        writeln!(&mut file, "{zone_formatter_template}").unwrap();
    }

    {
        let mut path_buf = path_buf.clone();
        path_buf.push("zoned_date_formatter.rs");
        let mut file = File::create(&path_buf).unwrap();
        use std::io::Write;
        writeln!(&mut file, "{zoned_date_formatter_template}").unwrap();
    }

    {
        let mut path_buf = path_buf.clone();
        path_buf.push("zoned_time_formatter.rs");
        let mut file = File::create(&path_buf).unwrap();
        use std::io::Write;
        writeln!(&mut file, "{zoned_time_formatter_template}").unwrap();
    }

    {
        let mut path_buf = path_buf.clone();
        path_buf.push("zoned_date_time_formatter.rs");
        let mut file = File::create(&path_buf).unwrap();
        use std::io::Write;
        writeln!(&mut file, "{zoned_datetime_formatter_template}").unwrap();
    }
}
