const std = @import("std");
const Allocator = std.mem.Allocator;
const bun = @import("bun");

pub const css = @import("./css_parser.zig");
pub const css_values = @import("./values/values.zig");
const DashedIdent = css_values.ident.DashedIdent;
pub const Error = css.Error;
const Location = css.Location;
const PrintErr = css.PrintErr;

const ArrayList = std.ArrayListUnmanaged;

const sourcemap = @import("./sourcemap.zig");

/// Options that control how CSS is serialized to a string.
pub const PrinterOptions = struct {
    /// Whether to minify the CSS, i.e. remove white space.
    minify: bool = false,
    /// An optional reference to a source map to write mappings into.
    /// (Available when the `sourcemap` feature is enabled.)
    source_map: ?*sourcemap.SourceMap = null,
    /// An optional project root path, used to generate relative paths for sources used in CSS module hashes.
    project_root: ?[]const u8 = null,
    /// Targets to output the CSS for.
    targets: Targets,
    /// Whether to analyze dependencies (i.e. `@import` and `url()`).
    /// If true, the dependencies are returned as part of the
    /// [ToCssResult](super::stylesheet::ToCssResult).
    ///
    /// When enabled, `@import` and `url()` dependencies
    /// are replaced with hashed placeholders that can be replaced with the final
    /// urls later (after bundling).
    analyze_dependencies: ?css.dependencies.DependencyOptions = null,
    /// A mapping of pseudo classes to replace with class names that can be applied
    /// from JavaScript. Useful for polyfills, for example.
    pseudo_classes: ?PseudoClasses = null,
    public_path: []const u8 = "",

    pub fn default() PrinterOptions {
        return .{
            .targets = Targets{
                .browsers = null,
            },
        };
    }

    pub fn defaultWithMinify(minify: bool) PrinterOptions {
        return .{
            .targets = Targets{
                .browsers = null,
            },
            .minify = minify,
        };
    }
};

/// A mapping of user action pseudo classes to replace with class names.
///
/// See [PrinterOptions](PrinterOptions).
const PseudoClasses = struct {
    /// The class name to replace `:hover` with.
    hover: ?[]const u8 = null,
    /// The class name to replace `:active` with.
    active: ?[]const u8 = null,
    /// The class name to replace `:focus` with.
    focus: ?[]const u8 = null,
    /// The class name to replace `:focus-visible` with.
    focus_visible: ?[]const u8 = null,
    /// The class name to replace `:focus-within` with.
    focus_within: ?[]const u8 = null,
};

pub const Targets = css.targets.Targets;

pub const Features = css.targets.Features;


pub const ImportInfo = struct {
    import_records: *const bun.BabyList(bun.ImportRecord),
    /// bundle_v2.graph.ast.items(.url_for_css)
    ast_urls_for_css: []const []const u8,
    /// bundle_v2.graph.input_files.items(.unique_key_for_additional_file)
    ast_unique_key_for_additional_file: []const []const u8,

    /// Only safe to use when outside the bundler. As in, the import records
    /// were not resolved to source indices. This will out-of-bounds otherwise.
    pub fn initOutsideOfBundler(records: *bun.BabyList(bun.ImportRecord)) ImportInfo {
        return .{
            .import_records = records,
            .ast_urls_for_css = &.{},
            .ast_unique_key_for_additional_file = &.{},
        };
    }
};

/// A `Printer` represents a destination to output serialized CSS, as used in
/// the [ToCss](super::traits::ToCss) trait. It can wrap any destination that
/// implements [std::fmt::Write](std::fmt::Write), such as a [String](String).
///
/// A `Printer` keeps track of the current line and column position, and uses
/// this to generate a source map if provided in the options.
///
/// `Printer` also includes helper functions that assist with writing output
/// that respects options such as `minify`, and `css_modules`.
pub fn Printer(comptime Writer: type) type {
    return struct {
        // #[cfg(feature = "sourcemap")]
        sources: ?*const ArrayList([]const u8),
        dest: Writer,
        loc: Location = Location{
            .source_index = 0,
            .line = 0,
            .column = 1,
        },
        indent_amt: u8 = 0,
        line: u32 = 0,
        col: u32 = 0,
        minify: bool,
        targets: Targets,
        vendor_prefix: css.VendorPrefix = .{},
        in_calc: bool = false,
        css_module: ?css.CssModule = null,
        dependencies: ?ArrayList(css.Dependency) = null,
        remove_imports: bool,
        /// A mapping of pseudo classes to replace with class names that can be applied
        /// from JavaScript. Useful for polyfills, for example.
        pseudo_classes: ?PseudoClasses = null,
        indentation_buf: std.ArrayList(u8),
        ctx: ?*const css.StyleContext = null,
        scratchbuf: std.ArrayList(u8),
        error_kind: ?css.PrinterError = null,
        import_info: ?ImportInfo = null,
        public_path: []const u8,
        symbols: *const bun.JSAst.Symbol.Map,
        local_names: ?*const css.LocalsResultsMap = null,
        /// NOTE This should be the same mimalloc heap arena allocator
        allocator: Allocator,
        // TODO: finish the fields

        pub threadlocal var in_debug_fmt: if (bun.Environment.isDebug) bool else u0 = if (bun.Environment.isDebug) false else 0;

        const This = @This();

        pub fn lookupSymbol(this: *This, ref: bun.bundle_v2.Ref) []const u8 {
            const symbols = this.symbols;

            const final_ref = symbols.follow(ref);
            if (this.local_names) |local_names| {
                if (local_names.get(final_ref)) |local_name| return local_name;
            }

            const original_name = symbols.get(final_ref).?.original_name;
            return original_name;
        }

        pub fn lookupIdentOrRef(this: *This, ident: css.css_values.ident.IdentOrRef) []const u8 {
            if (comptime bun.Environment.isDebug) {
                if (in_debug_fmt) {
                    return ident.debugIdent();
                }
            }
            if (ident.isIdent()) {
                return ident.asIdent().?.v;
            }
            return this.lookupSymbol(ident.asRef().?);
        }

        inline fn getWrittenAmt(writer: Writer) usize {
            return switch (Writer) {
                ArrayList(u8).Writer => writer.context.self.items.len,
                *bun.js_printer.BufferWriter => writer.written.len,
                else => @compileError("Dunno what to do with this type yo: " ++ @typeName(Writer)),
            };
        }

        /// Returns the current source filename that is being printed.
        pub fn filename(this: *const This) []const u8 {
            if (this.sources) |sources| {
                if (this.loc.source_index < sources.items.len) return sources.items[this.loc.source_index];
            }
            return "unknown.css";
        }

        /// Returns whether the indent level is greater than one.
        pub fn isNested(this: *const This) bool {
            return this.indent_amt > 2;
        }

        /// Add an error related to std lib fmt errors
        pub fn addFmtError(this: *This) PrintErr {
            this.error_kind = css.PrinterError{
                .kind = .fmt_error,
                .loc = null,
            };
            return PrintErr.CSSPrintError;
        }

        pub fn addNoImportRecordError(this: *This) PrintErr {
            this.error_kind = css.PrinterError{
                .kind = .no_import_records,
                .loc = null,
            };
            return PrintErr.CSSPrintError;
        }

        pub fn addInvalidCssModulesPatternInGridError(this: *This) PrintErr {
            this.error_kind = css.PrinterError{
                .kind = .invalid_css_modules_pattern_in_grid,
                .loc = css.ErrorLocation{
                    .filename = this.filename(),
                    .line = this.loc.line,
                    .column = this.loc.column,
                },
            };
            return PrintErr.CSSPrintError;
        }

        /// Returns an error of the given kind at the provided location in the current source file.
        pub fn newError(
            this: *This,
            kind: css.PrinterErrorKind,
            maybe_loc: ?css.dependencies.Location,
        ) PrintErr!void {
            bun.debugAssert(this.error_kind == null);
            this.error_kind = css.PrinterError{
                .kind = kind,
                .loc = if (maybe_loc) |loc| css.ErrorLocation{
                    .filename = this.filename(),
                    .line = loc.line - 1,
                    .column = loc.column,
                } else null,
            };
            return PrintErr.CSSPrintError;
        }

        pub fn deinit(this: *This) void {
            this.scratchbuf.deinit();
            this.indentation_buf.deinit();
            if (this.dependencies) |*dependencies| {
                dependencies.deinit(this.allocator);
            }
        }

        /// If `import_records` is null, then the printer will error when it encounters code that relies on import records (urls())
        pub fn new(
            allocator: Allocator,
            scratchbuf: std.ArrayList(u8),
            dest: Writer,
            options: PrinterOptions,
            import_info: ?ImportInfo,
            local_names: ?*const css.LocalsResultsMap,
            symbols: *const bun.JSAst.Symbol.Map,
        ) This {
            return .{
                .sources = null,
                .dest = dest,
                .minify = options.minify,
                .targets = options.targets,
                .dependencies = if (options.analyze_dependencies != null) .empty else null,
                .remove_imports = options.analyze_dependencies != null and options.analyze_dependencies.?.remove_imports,
                .pseudo_classes = options.pseudo_classes,
                .indentation_buf = .init(allocator),
                .import_info = import_info,
                .scratchbuf = scratchbuf,
                .allocator = allocator,
                .public_path = options.public_path,
                .local_names = local_names,
                .loc = .{
                    .source_index = 0,
                    .line = 0,
                    .column = 1,
                },
                .symbols = symbols,
            };
        }

        pub inline fn getImportRecords(this: *This) PrintErr!*const bun.BabyList(bun.ImportRecord) {
            if (this.import_info) |info| return info.import_records;
            return this.addNoImportRecordError();
        }

        pub fn printImportRecord(this: *This, import_record_idx: u32) PrintErr!void {
            if (this.import_info) |info| {
                const import_record = info.import_records.at(import_record_idx);
                const a, const b = bun.bundle_v2.cheapPrefixNormalizer(this.public_path, import_record.path.text);
                try this.writeStr(a);
                try this.writeStr(b);
                return;
            }
            return this.addNoImportRecordError();
        }

        pub inline fn importRecord(this: *Printer(Writer), import_record_idx: u32) PrintErr!*const bun.ImportRecord {
            if (this.import_info) |info| return info.import_records.at(import_record_idx);
            return this.addNoImportRecordError();
        }

        pub inline fn getImportRecordUrl(this: *This, import_record_idx: u32) PrintErr![]const u8 {
            const import_info = this.import_info orelse return this.addNoImportRecordError();
            const record = import_info.import_records.at(import_record_idx);
            if (record.source_index.isValid()) {
                // It has an inlined url for CSS
                const urls_for_css = import_info.ast_urls_for_css[record.source_index.get()];
                if (urls_for_css.len > 0) {
                    return urls_for_css;
                }
                // It is a chunk URL
                const unique_key_for_additional_file = import_info.ast_unique_key_for_additional_file[record.source_index.get()];
                if (unique_key_for_additional_file.len > 0) {
                    return unique_key_for_additional_file;
                }
            }
            // External URL stays as-is
            return record.path.text;
        }

        pub fn context(this: *const Printer(Writer)) ?*const css.StyleContext {
            return this.ctx;
        }

        /// To satisfy io.Writer interface
        ///
        /// NOTE: Same constraints as `writeStr`, the `str` param is assumted to not
        /// contain any newline characters
        pub fn writeAll(this: *This, str: []const u8) !void {
            return this.writeStr(str) catch std.mem.Allocator.Error.OutOfMemory;
        }

        pub fn writeComment(this: *This, comment: []const u8) PrintErr!void {
            _ = this.dest.writeAll(comment) catch {
                return this.addFmtError();
            };
            const new_lines = std.mem.count(u8, comment, "\n");
            this.line += @intCast(new_lines);
            this.col = 0;
            const last_line_start = comment.len - (std.mem.lastIndexOfScalar(u8, comment, '\n') orelse comment.len);
            this.col += @intCast(last_line_start);
            return;
        }

        /// Writes a raw string to the underlying destination.
        ///
        /// NOTE: Is is assumed that the string does not contain any newline characters.
        /// If such a string is written, it will break source maps.
        pub fn writeStr(this: *This, s: []const u8) PrintErr!void {
            if (comptime bun.Environment.isDebug) {
                bun.assert(std.mem.indexOfScalar(u8, s, '\n') == null);
            }
            this.col += @intCast(s.len);
            _ = this.dest.writeAll(s) catch {
                return this.addFmtError();
            };
            return;
        }

        /// Writes a formatted string to the underlying destination.
        ///
        /// NOTE: Is is assumed that the formatted string does not contain any newline characters.
        /// If such a string is written, it will break source maps.
        pub fn writeFmt(this: *This, comptime fmt: []const u8, args: anytype) PrintErr!void {
            // assuming the writer comes from an ArrayList
            const start: usize = getWrittenAmt(this.dest);
            this.dest.print(fmt, args) catch bun.outOfMemory();
            const written = getWrittenAmt(this.dest) - start;
            this.col += @intCast(written);
        }

        fn replaceDots(allocator: Allocator, s: []const u8) []const u8 {
            var str = allocator.dupe(u8, s) catch bun.outOfMemory();
            std.mem.replaceScalar(u8, str[0..], '.', '-');
            return str;
        }

        pub fn writeIdentOrRef(this: *This, ident: css.css_values.ident.IdentOrRef, handle_css_module: bool) PrintErr!void {
            if (!handle_css_module) {
                if (ident.asIdent()) |identifier| {
                    return css.serializer.serializeIdentifier(identifier.v, this) catch return this.addFmtError();
                } else {
                    const ref = ident.asRef().?;
                    const symbol = this.symbols.get(ref) orelse return this.addFmtError();
                    return css.serializer.serializeIdentifier(symbol.original_name, this) catch return this.addFmtError();
                }
            }

            const str = this.lookupIdentOrRef(ident);
            return css.serializer.serializeIdentifier(str, this) catch return this.addFmtError();
        }

        /// Writes a CSS identifier to the underlying destination, escaping it
        /// as appropriate. If the `css_modules` option was enabled, then a hash
        /// is added, and the mapping is added to the CSS module.
        pub fn writeIdent(this: *This, ident: []const u8, handle_css_module: bool) PrintErr!void {
            if (handle_css_module) {
                if (this.css_module) |*css_module| {
                    const Closure = struct { first: bool, printer: *This };
                    var closure = Closure{ .first = true, .printer = this };
                    css_module.config.pattern.write(
                        css_module.hashes.items[this.loc.source_index],
                        css_module.sources.items[this.loc.source_index],
                        ident,
                        &closure,
                        struct {
                            pub fn writeFn(self: *Closure, s1: []const u8, replace_dots: bool) void {
                                // PERF: stack fallback?
                                const s = if (!replace_dots) s1 else replaceDots(self.printer.allocator, s1);
                                defer if (replace_dots) self.printer.allocator.free(s);
                                self.printer.col += @intCast(s.len);
                                if (self.first) {
                                    self.first = false;
                                    return css.serializer.serializeIdentifier(s, self.printer) catch |e| css.OOM(e);
                                } else {
                                    return css.serializer.serializeName(s, self.printer) catch |e| css.OOM(e);
                                }
                            }
                        }.writeFn,
                    );

                    css_module.addLocal(this.allocator, ident, ident, this.loc.source_index);
                    return;
                }
            }

            return css.serializer.serializeIdentifier(ident, this) catch return this.addFmtError();
        }

        pub fn writeDashedIdent(this: *This, ident: *const DashedIdent, is_declaration: bool) !void {
            try this.writeStr("--");

            if (this.css_module) |*css_module| {
                if (css_module.config.dashed_idents) {
                    const Fn = struct {
                        pub fn writeFn(self: *This, s1: []const u8, replace_dots: bool) void {
                            const s = if (!replace_dots) s1 else replaceDots(self.allocator, s1);
                            defer if (replace_dots) self.allocator.free(s);
                            self.col += @intCast(s.len);
                            return css.serializer.serializeName(s, self) catch |e| css.OOM(e);
                        }
                    };
                    css_module.config.pattern.write(
                        css_module.hashes.items[this.loc.source_index],
                        css_module.sources.items[this.loc.source_index],
                        ident.v[2..],
                        this,
                        Fn.writeFn,
                    );

                    if (is_declaration) {
                        css_module.addDashed(this.allocator, ident.v, this.loc.source_index);
                    }
                }
            }

            return css.serializer.serializeName(ident.v[2..], this) catch return this.addFmtError();
        }

        pub fn writeByte(this: *This, char: u8) !void {
            return this.writeChar(char) catch return Allocator.Error.OutOfMemory;
        }

        /// Write a single character to the underlying destination.
        pub fn writeChar(this: *This, char: u8) PrintErr!void {
            if (char == '\n') {
                this.line += 1;
                this.col = 0;
            } else {
                this.col += 1;
            }
            _ = this.dest.writeByte(char) catch {
                return this.addFmtError();
            };
        }

        /// Writes a newline character followed by indentation.
        /// If the `minify` option is enabled, then nothing is printed.
        pub fn newline(this: *This) PrintErr!void {
            if (this.minify) {
                return;
            }

            try this.writeChar('\n');
            return this.writeIndent();
        }

        /// Writes a delimiter character, followed by whitespace (depending on the `minify` option).
        /// If `ws_before` is true, then whitespace is also written before the delimiter.
        pub fn delim(this: *This, delim_: u8, ws_before: bool) PrintErr!void {
            if (ws_before) {
                try this.whitespace();
            }
            try this.writeChar(delim_);
            return this.whitespace();
        }

        /// Writes a single whitespace character, unless the `minify` option is enabled.
        ///
        /// Use `write_char` instead if you wish to force a space character to be written,
        /// regardless of the `minify` option.
        pub fn whitespace(this: *This) PrintErr!void {
            if (this.minify) return;
            return this.writeChar(' ');
        }

        pub fn withContext(
            this: *This,
            selectors: *const css.SelectorList,
            closure: anytype,
            comptime func: anytype,
        ) PrintErr!void {
            const parent = if (this.ctx) |ctx| parent: {
                this.ctx = null;
                break :parent ctx;
            } else null;

            const ctx = css.StyleContext{ .selectors = selectors, .parent = parent };

            this.ctx = &ctx;
            const res = func(closure, Writer, this);
            this.ctx = parent;

            return res;
        }

        pub fn withClearedContext(
            this: *This,
            closure: anytype,
            comptime func: anytype,
        ) PrintErr!void {
            const parent = if (this.ctx) |ctx| parent: {
                this.ctx = null;
                break :parent ctx;
            } else null;
            const res = func(closure, Writer, this);
            this.ctx = parent;
            return res;
        }

        /// Increases the current indent level.
        pub fn indent(this: *This) void {
            this.indent_amt += 2;
        }

        /// Decreases the current indent level.
        pub fn dedent(this: *This) void {
            this.indent_amt -= 2;
        }

        const INDENTS: []const []const u8 = indents: {
            const levels = 32;
            var indents: [levels][]const u8 = undefined;
            for (0..levels) |i| {
                const n = i * 2;
                var str: [n]u8 = undefined;
                for (0..n) |j| {
                    str[j] = ' ';
                }
                indents[i] = str;
            }
            break :indents indents;
        };

        fn getIndent(this: *This, idnt: u8) []const u8 {
            // divide by 2 to get index into table
            const i = idnt >> 1;
            // PERF: may be faster to just do `i < (IDENTS.len - 1) * 2` (e.g. 62 if IDENTS.len == 32) here
            if (i < INDENTS.len) {
                return INDENTS[i];
            }
            if (this.indentation_buf.items.len < idnt) {
                this.indentation_buf.appendNTimes(' ', this.indentation_buf.items.len - idnt) catch unreachable;
            } else {
                this.indentation_buf.items = this.indentation_buf.items[0..idnt];
            }
            return this.indentation_buf.items;
        }

        fn writeIndent(this: *This) PrintErr!void {
            bun.debugAssert(!this.minify);
            if (this.indent_amt > 0) {
                // try this.writeStr(this.getIndent(this.ident));
                this.dest.writeByteNTimes(' ', this.indent_amt) catch return this.addFmtError();
            }
        }
    };
}
