import SwiftUI
import AppKit

/// Editing state for the Editor pane, shared via the environment so the pane's
/// header controls (which live in `PanelContainer`) and its body can both drive it.
/// One model backs the workspace's editing — selection is already shared through
/// `FileSelectionStore`, so a single buffer is the right granularity.
@MainActor
@Observable
final class EditorModel {
    enum Status: Equatable {
        case empty            // nothing selected
        case editing          // text is editable
        case readOnly(String) // binary/oversized — note explains why
    }

    /// For markdown files, show the raw source or a rendered preview.
    enum Mode { case edit, preview }

    private(set) var url: URL?
    private(set) var original = ""
    var text = ""
    private(set) var status: Status = .empty
    var mode: Mode = .edit
    var errorMessage: String?

    /// Largest file we'll load into the editor; bigger files are too heavy for a
    /// `TextEditor` and are reported instead of opened.
    private static let editableCap = 2 * 1024 * 1024
    private static let markdownExtensions: Set<String> = ["md", "markdown", "mdown", "mkd", "mkdn"]

    var isMarkdown: Bool {
        guard let url else { return false }
        return Self.markdownExtensions.contains(url.pathExtension.lowercased())
    }
    /// Syntax rules for the open file, derived from its extension (`.plain` when
    /// unknown). Drives the editor's coloring.
    var language: LanguageSyntax { LanguageSyntax.forFile(url) }
    var isEditing: Bool { status == .editing }
    var isDirty: Bool { status == .editing && text != original }

    /// Load the file at `newURL`. A no-op if that file is already open, so reloads
    /// don't clobber in-progress edits.
    func load(_ newURL: URL?) {
        errorMessage = nil
        guard let newURL else { reset(.empty); return }
        if url == newURL { return }

        var isDir: ObjCBool = false
        FileManager.default.fileExists(atPath: newURL.path, isDirectory: &isDir)
        if isDir.boolValue { reset(.empty); return }

        guard let attrs = try? FileManager.default.attributesOfItem(atPath: newURL.path),
              let size = attrs[.size] as? Int else {
            url = newURL; reset(.readOnly("Couldn't read this file.")); return
        }
        if size > Self.editableCap {
            url = newURL; reset(.readOnly("File is too large to edit (\(size / 1024) KB).")); return
        }
        guard let data = try? Data(contentsOf: newURL),
              let string = String(data: data, encoding: .utf8) else {
            url = newURL; reset(.readOnly("Binary file — no text preview.")); return
        }
        url = newURL
        original = string
        text = string
        status = .editing
        mode = isMarkdown ? .preview : .edit
    }

    func revert() { text = original }

    func save() {
        guard let url, status == .editing else { return }
        do {
            try text.write(to: url, atomically: true, encoding: .utf8)
            original = text
            errorMessage = nil
        } catch {
            errorMessage = "Save failed: \(error.localizedDescription)"
        }
    }

    private func reset(_ status: Status) {
        original = ""; text = ""; self.status = status
        if case .empty = status { url = nil }
    }
}

/// A preview/editor for the file currently selected in the Files explorer. The
/// title and the Edit/Preview/Revert/Save controls live in the pane header
/// (`EditorHeaderControls`); this view is just the text/preview surface.
struct EditorPanel: View {
    @Environment(EditorModel.self) private var editor
    @Environment(FileSelectionStore.self) private var selection
    @AppStorage(ColorVisionMode.storageKey) private var colorVisionRaw = ColorVisionMode.standard.rawValue

    private var colorVision: ColorVisionMode {
        ColorVisionMode(rawValue: colorVisionRaw) ?? .standard
    }

    var body: some View {
        content
            .task(id: selection.selected) { editor.load(selection.selected) }
    }

    @ViewBuilder
    private var content: some View {
        @Bindable var editor = editor
        switch editor.status {
        case .empty:
            placeholder("doc.text.magnifyingglass", "Select a file in the Files panel to open it here.")
        case .readOnly(let note):
            placeholder("doc.questionmark", note)
        case .editing:
            VStack(spacing: 0) {
                if editor.isMarkdown, editor.mode == .preview {
                    ScrollView {
                        MarkdownText(markdown: editor.text)
                            .textSelection(.enabled)
                            .padding(12)
                            .frame(maxWidth: .infinity, alignment: .leading)
                    }
                    .background(AppTheme.background)
                } else {
                    CodeEditorView(text: $editor.text, language: editor.language,
                                   colorVision: colorVision)
                        .background(AppTheme.background)
                }
                if let errorMessage = editor.errorMessage {
                    Text(errorMessage)
                        .font(.caption).foregroundStyle(.white)
                        .frame(maxWidth: .infinity, alignment: .leading)
                        .padding(6).background(Color.red.opacity(0.85))
                }
            }
        }
    }

    private func placeholder(_ symbol: String, _ message: String) -> some View {
        VStack(spacing: 8) {
            Image(systemName: symbol).font(.largeTitle).foregroundStyle(.tertiary)
            Text(message)
                .font(.callout).foregroundStyle(.secondary)
                .multilineTextAlignment(.center)
        }
        .padding(24)
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}

/// Icon controls for the Editor pane's header: a markdown preview toggle, an
/// unsaved-changes dot, and Revert/Save. Reads the shared `EditorModel`, so it
/// works from inside the `PanelContainer` header.
struct EditorHeaderControls: View {
    @Environment(EditorModel.self) private var editor
    @Environment(\.appPalette) private var palette

    var body: some View {
        if editor.isEditing {
            HStack(spacing: 4) {
                if editor.isMarkdown {
                    // Show the mode the button will switch *to*: a pencil to go edit
                    // the source, an eye to see the rendered preview.
                    iconButton(
                        editor.mode == .preview ? "pencil" : "eye",
                        help: editor.mode == .preview ? "Edit source" : "Preview rendered markdown",
                        tint: .secondary
                    ) {
                        editor.mode = editor.mode == .preview ? .edit : .preview
                    }
                }
                if editor.isDirty {
                    Circle().fill(palette.attention).frame(width: 6, height: 6)
                        .help("Unsaved changes")
                }
                iconButton("arrow.uturn.backward", help: "Revert changes",
                           tint: .secondary, disabled: !editor.isDirty) { editor.revert() }
                iconButton("internaldrive", help: "Save",
                           tint: editor.isDirty ? palette.accent : .secondary,
                           disabled: !editor.isDirty) { editor.save() }
            }
        }
    }

    private func iconButton(
        _ systemName: String, help: String, tint: Color,
        disabled: Bool = false, action: @escaping () -> Void
    ) -> some View {
        Button(action: action) {
            Image(systemName: systemName)
                .font(.system(size: 13))
                // A roomy, fully-clickable target around the small glyph.
                .frame(width: 24, height: 24)
                .contentShape(Rectangle())
        }
        .buttonStyle(.plain)
        .foregroundStyle(tint)
        .disabled(disabled)
        .help(help)
    }
}
