module fortplot_spec_json_channels
    !! JSON channel, scale, domain, and axis parsing.
    !!
    !! Handles encoding channels (x, y, color, etc.), scale configuration,
    !! axis properties, and supported domain parsing.

    use, intrinsic :: iso_fortran_env, only: wp => real64
    use fortplot_spec_types, only: channel_t, scale_t, axis_t, &
                                   mark_t, encoding_t
    use fortplot_spec_json_reader, only: skip_ws, expect_char, &
                                          read_string, read_real, &
                                          read_bool, read_literal, skip_value

    implicit none
    private

    public :: parse_channel, parse_scale, parse_axis
    public :: parse_supported_domain, parse_domain
    public :: parse_real_array
    public :: parse_mark, parse_encoding

contains

    subroutine parse_channel(json, pos, ch, status)
        !! Parse a single encoding channel object
        character(len=*), intent(in) :: json
        integer, intent(inout) :: pos
        type(channel_t), intent(out) :: ch
        integer, intent(out) :: status
        character(len=:), allocatable :: key

        status = 0
        ch%defined = .true.

        if (.not. expect_char(json, pos, '{')) then
            status = 30
            return
        end if

        do
            call skip_ws(json, pos)
            if (json(pos:pos) == ',') pos = pos + 1
            call skip_ws(json, pos)
            if (json(pos:pos) == '}') then
                pos = pos + 1
                return
            end if

            call read_string(json, pos, key, status)
            if (status /= 0) return
            call skip_ws(json, pos)
            if (.not. expect_char(json, pos, ':')) then
                status = 31
                return
            end if
            call skip_ws(json, pos)

            select case (key)
            case ('field')
                call read_string(json, pos, ch%field, status)
            case ('type')
                call read_string(json, pos, ch%type, status)
            case ('value')
                call read_literal(json, pos, ch%value, status)
            case ('scale')
                call parse_scale(json, pos, ch%scale, status)
            case ('axis')
                call parse_axis(json, pos, ch%axis, status)
            case default
                call skip_value(json, pos)
            end select

            if (status /= 0) return
        end do
    end subroutine parse_channel

    subroutine parse_scale(json, pos, sc, status)
        !! Parse scale object
        character(len=*), intent(in) :: json
        integer, intent(inout) :: pos
        type(scale_t), intent(out) :: sc
        integer, intent(out) :: status
        character(len=:), allocatable :: key

        status = 0
        if (.not. expect_char(json, pos, '{')) then
            status = 40
            return
        end if

        do
            call skip_ws(json, pos)
            if (json(pos:pos) == ',') pos = pos + 1
            call skip_ws(json, pos)
            if (json(pos:pos) == '}') then
                pos = pos + 1
                return
            end if

            call read_string(json, pos, key, status)
            if (status /= 0) return
            call skip_ws(json, pos)
            if (.not. expect_char(json, pos, ':')) then
                status = 41
                return
            end if
            call skip_ws(json, pos)

            select case (key)
            case ('type')
                call read_string(json, pos, sc%type, status)
            case ('domain')
                call parse_supported_domain(json, pos, sc, status)
            case ('zero')
                call read_bool(json, pos, sc%zero, status)
            case ('exponent')
                call read_real(json, pos, sc%exponent, status)
            case default
                call skip_value(json, pos)
            end select

            if (status /= 0) return
        end do
    end subroutine parse_scale

    subroutine parse_supported_domain(json, pos, sc, status)
        !! Parse only numeric domains and skip categorical domains.
        character(len=*), intent(in) :: json
        integer, intent(inout) :: pos
        type(scale_t), intent(inout) :: sc
        integer, intent(out) :: status

        integer :: probe

        status = 0
        probe = pos
        call skip_ws(json, probe)
        if (.not. expect_char(json, probe, '[')) then
            call skip_value(json, pos)
            return
        end if
        call skip_ws(json, probe)
        if (probe > len(json)) then
            status = 42
            return
        end if

        select case (json(probe:probe))
        case ('-', '0':'9')
            call parse_domain(json, pos, sc, status)
        case default
            call skip_value(json, pos)
        end select
    end subroutine parse_supported_domain

    subroutine parse_domain(json, pos, sc, status)
        !! Parse domain array [min, max]
        character(len=*), intent(in) :: json
        integer, intent(inout) :: pos
        type(scale_t), intent(inout) :: sc
        integer, intent(out) :: status

        status = 0
        if (.not. expect_char(json, pos, '[')) then
            status = 42
            return
        end if
        call skip_ws(json, pos)
        call read_real(json, pos, sc%domain_min, status)
        if (status /= 0) return
        call skip_ws(json, pos)
        if (.not. expect_char(json, pos, ',')) then
            status = 43
            return
        end if
        call skip_ws(json, pos)
        call read_real(json, pos, sc%domain_max, status)
        if (status /= 0) return
        call skip_ws(json, pos)
        if (.not. expect_char(json, pos, ']')) then
            status = 44
            return
        end if
        sc%domain_set = .true.
    end subroutine parse_domain

    subroutine parse_axis(json, pos, ax, status)
        !! Parse axis object
        character(len=*), intent(in) :: json
        integer, intent(inout) :: pos
        type(axis_t), intent(out) :: ax
        integer, intent(out) :: status
        character(len=:), allocatable :: key

        status = 0
        if (.not. expect_char(json, pos, '{')) then
            status = 50
            return
        end if

        do
            call skip_ws(json, pos)
            if (json(pos:pos) == ',') pos = pos + 1
            call skip_ws(json, pos)
            if (json(pos:pos) == '}') then
                pos = pos + 1
                return
            end if

            call read_string(json, pos, key, status)
            if (status /= 0) return
            call skip_ws(json, pos)
            if (.not. expect_char(json, pos, ':')) then
                status = 51
                return
            end if
            call skip_ws(json, pos)

            select case (key)
            case ('title')
                call read_string(json, pos, ax%title, status)
                ax%title_set = .true.
            case ('grid')
                call read_bool(json, pos, ax%grid, status)
            case ('labelAngle')
                call read_real(json, pos, ax%label_angle, status)
            case ('values')
                call parse_real_array(json, pos, ax%tick_values, &
                                     status)
            case ('format')
                call read_string(json, pos, ax%format, status)
            case ('gridOpacity')
                call read_real(json, pos, ax%grid_opacity, status)
            case ('gridDash')
                call skip_value(json, pos)
            case default
                call skip_value(json, pos)
            end select

            if (status /= 0) return
        end do
    end subroutine parse_axis

    subroutine parse_real_array(json, pos, values, status)
        character(len=*), intent(in) :: json
        integer, intent(inout) :: pos
        real(wp), allocatable, intent(out) :: values(:)
        integer, intent(out) :: status
        integer :: count, save_pos, i
        real(wp) :: tmp

        status = 0
        if (.not. expect_char(json, pos, '[')) then
            status = 97
            return
        end if

        call skip_ws(json, pos)
        if (json(pos:pos) == ']') then
            pos = pos + 1
            allocate (values(0))
            return
        end if

        save_pos = pos
        count = 0
        do
            if (count > 0) then
                if (.not. expect_char(json, pos, ',')) then
                    status = 98
                    return
                end if
                call skip_ws(json, pos)
            end if
            call read_real(json, pos, tmp, status)
            if (status /= 0) return
            count = count + 1
            call skip_ws(json, pos)
            if (json(pos:pos) == ']') exit
        end do

        allocate (values(count))
        pos = save_pos
        do i = 1, count
            if (i > 1) then
                if (.not. expect_char(json, pos, ',')) then
                    status = 99
                    return
                end if
                call skip_ws(json, pos)
            end if
            call read_real(json, pos, values(i), status)
            if (status /= 0) return
            call skip_ws(json, pos)
        end do
        if (.not. expect_char(json, pos, ']')) then
            status = 102
        end if
    end subroutine parse_real_array

    subroutine parse_mark(json, pos, m, status)
        !! Parse mark: either a string or an object
        character(len=*), intent(in) :: json
        integer, intent(inout) :: pos
        type(mark_t), intent(out) :: m
        integer, intent(out) :: status
        character(len=:), allocatable :: key

        status = 0
        call skip_ws(json, pos)

        if (json(pos:pos) == '"') then
            call read_string(json, pos, m%type, status)
            return
        end if

        if (.not. expect_char(json, pos, '{')) then
            status = 10
            return
        end if

        do
            call skip_ws(json, pos)
            if (json(pos:pos) == ',') pos = pos + 1
            call skip_ws(json, pos)
            if (json(pos:pos) == '}') then
                pos = pos + 1
                return
            end if

            call read_string(json, pos, key, status)
            if (status /= 0) return
            call skip_ws(json, pos)
            if (.not. expect_char(json, pos, ':')) then
                status = 11
                return
            end if
            call skip_ws(json, pos)

            select case (key)
            case ('type')
                call read_string(json, pos, m%type, status)
            case ('size')
                call read_real(json, pos, m%size, status)
            case ('opacity')
                call read_real(json, pos, m%opacity, status)
            case ('strokeWidth')
                call read_real(json, pos, m%stroke_width, status)
            case ('strokeDash')
                call parse_real_array(json, pos, m%stroke_dash, status)
            case ('stroke')
                call read_string(json, pos, m%stroke, status)
            case ('fill')
                call read_string(json, pos, m%fill, status)
            case ('interpolate')
                call read_string(json, pos, m%interpolate, status)
            case ('point')
                call read_literal(json, pos, m%point, status)
            case ('filled')
                call read_bool(json, pos, m%filled, status)
            case default
                call skip_value(json, pos)
            end select

            if (status /= 0) return
        end do
    end subroutine parse_mark

    subroutine parse_encoding(json, pos, enc, status)
        !! Parse encoding object with named channels
        character(len=*), intent(in) :: json
        integer, intent(inout) :: pos
        type(encoding_t), intent(out) :: enc
        integer, intent(out) :: status
        character(len=:), allocatable :: key

        status = 0
        if (.not. expect_char(json, pos, '{')) then
            status = 20
            return
        end if

        do
            call skip_ws(json, pos)
            if (json(pos:pos) == ',') pos = pos + 1
            call skip_ws(json, pos)
            if (json(pos:pos) == '}') then
                pos = pos + 1
                return
            end if

            call read_string(json, pos, key, status)
            if (status /= 0) return
            call skip_ws(json, pos)
            if (.not. expect_char(json, pos, ':')) then
                status = 21
                return
            end if
            call skip_ws(json, pos)

            select case (key)
            case ('x')
                call parse_channel(json, pos, enc%x, status)
            case ('y')
                call parse_channel(json, pos, enc%y, status)
            case ('x2')
                call parse_channel(json, pos, enc%x2, status)
            case ('y2')
                call parse_channel(json, pos, enc%y2, status)
            case ('color')
                call parse_channel(json, pos, enc%color, status)
            case ('size')
                call parse_channel(json, pos, enc%size, status)
            case ('shape')
                call parse_channel(json, pos, enc%shape, status)
            case ('opacity')
                call parse_channel(json, pos, enc%opacity, status)
            case ('text')
                call parse_channel(json, pos, enc%text, status)
            case default
                call skip_value(json, pos)
            end select

            if (status /= 0) return
        end do
    end subroutine parse_encoding

end module fortplot_spec_json_channels
