module fortplot_spec_builder
    !! Builder API for constructing spec_t from data arrays.

    use, intrinsic :: iso_fortran_env, only: wp => real64
    use fortplot_spec_json, only: spec_to_json_file
    use fortplot_spec_rendering, only: render_spec_to_file, spec_target_is_json
    use fortplot_spec_types, only: spec_t, channel_t, layer_t
    implicit none

    private
    public :: vl_line, vl_point, vl_bar, vl_area
    public :: vl_layer_add, vl_channel
    public :: spec_savefig

contains

    function vl_line(x, y, title, xlabel, ylabel, width, height, &
                     interpolate) result(spec)
        real(wp), contiguous, intent(in) :: x(:), y(:)
        character(len=*), intent(in), optional :: title
        character(len=*), intent(in), optional :: xlabel, ylabel
        integer, intent(in), optional :: width, height
        character(len=*), intent(in), optional :: interpolate
        type(spec_t) :: spec

        call init_xy_spec(spec, 'line', x, y, title, xlabel, ylabel, width, height)
        if (present(interpolate)) spec%mark%interpolate = interpolate
    end function vl_line

    function vl_point(x, y, title, xlabel, ylabel, width, height) result(spec)
        real(wp), contiguous, intent(in) :: x(:), y(:)
        character(len=*), intent(in), optional :: title
        character(len=*), intent(in), optional :: xlabel, ylabel
        integer, intent(in), optional :: width, height
        type(spec_t) :: spec

        call init_xy_spec(spec, 'point', x, y, title, xlabel, ylabel, width, height)
    end function vl_point

    function vl_bar(x, y, title, xlabel, ylabel, width, height) result(spec)
        real(wp), contiguous, intent(in) :: x(:), y(:)
        character(len=*), intent(in), optional :: title
        character(len=*), intent(in), optional :: xlabel, ylabel
        integer, intent(in), optional :: width, height
        type(spec_t) :: spec

        call init_xy_spec(spec, 'bar', x, y, title, xlabel, ylabel, width, height)
        spec%encoding%x%type = 'ordinal'
    end function vl_bar

    function vl_area(x, y, title, xlabel, ylabel, width, height) result(spec)
        real(wp), contiguous, intent(in) :: x(:), y(:)
        character(len=*), intent(in), optional :: title
        character(len=*), intent(in), optional :: xlabel, ylabel
        integer, intent(in), optional :: width, height
        type(spec_t) :: spec

        call init_xy_spec(spec, 'area', x, y, title, xlabel, ylabel, width, height)
    end function vl_area

    subroutine init_xy_spec(spec, mark_type, x, y, title, xlabel, ylabel, width, height)
        type(spec_t), intent(out) :: spec
        character(len=*), intent(in) :: mark_type
        real(wp), contiguous, intent(in) :: x(:), y(:)
        character(len=*), intent(in), optional :: title
        character(len=*), intent(in), optional :: xlabel, ylabel
        integer, intent(in), optional :: width, height
        integer :: n

        n = min(size(x), size(y))
        spec%mark%type = mark_type

        if (present(title)) spec%title = title
        if (present(width)) spec%width = width
        if (present(height)) spec%height = height

        spec%encoding%x = vl_channel('x', 'quantitative')
        spec%encoding%y = vl_channel('y', 'quantitative')
        if (present(xlabel)) then
            spec%encoding%x%axis%title = xlabel
            spec%encoding%x%axis%title_set = .true.
        end if
        if (present(ylabel)) then
            spec%encoding%y%axis%title = ylabel
            spec%encoding%y%axis%title_set = .true.
        end if

        allocate (spec%data%columns(2))
        spec%data%columns(1)%field = 'x'
        spec%data%columns(1)%values = x(1:n)
        spec%data%columns(2)%field = 'y'
        spec%data%columns(2)%values = y(1:n)
        spec%data%nrows = n
    end subroutine init_xy_spec

    function vl_channel(field, type) result(ch)
        character(len=*), intent(in) :: field, type
        type(channel_t) :: ch

        ch%field = field
        ch%type = type
        ch%defined = .true.
    end function vl_channel

    subroutine vl_layer_add(spec, mark_type, x, y, label, interpolate)
        type(spec_t), intent(inout) :: spec
        character(len=*), intent(in) :: mark_type
        real(wp), contiguous, intent(in) :: x(:), y(:)
        character(len=*), intent(in), optional :: label
        character(len=*), intent(in), optional :: interpolate
        type(layer_t), allocatable :: new_layers(:)
        type(layer_t) :: new_layer
        integer :: n
        integer :: old_count

        n = min(size(x), size(y))
        if (.not. spec%is_layered) call promote_to_layered(spec)

        new_layer%mark%type = mark_type
        new_layer%encoding%x = vl_channel('x', 'quantitative')
        new_layer%encoding%y = vl_channel('y', 'quantitative')
        if (present(label)) then
            new_layer%encoding%color%value = '"'//label//'"'
            new_layer%encoding%color%defined = .true.
        end if
        if (present(interpolate)) new_layer%mark%interpolate = interpolate

        allocate (new_layer%data%columns(2))
        new_layer%data%columns(1)%field = 'x'
        new_layer%data%columns(1)%values = x(1:n)
        new_layer%data%columns(2)%field = 'y'
        new_layer%data%columns(2)%values = y(1:n)
        new_layer%data%nrows = n
        new_layer%has_data = .true.

        old_count = spec%layer_count
        allocate (new_layers(old_count + 1))
        if (old_count > 0) new_layers(1:old_count) = spec%layers(1:old_count)
        new_layers(old_count + 1) = new_layer
        call move_alloc(new_layers, spec%layers)
        spec%layer_count = old_count + 1
    end subroutine vl_layer_add

    subroutine promote_to_layered(spec)
        type(spec_t), intent(inout) :: spec

        spec%is_layered = .true.
        allocate (spec%layers(1))
        spec%layers(1)%mark = spec%mark
        spec%layers(1)%encoding = spec%encoding
        spec%layers(1)%has_data = .false.
        spec%layer_count = 1
    end subroutine promote_to_layered

    subroutine spec_savefig(spec, filename, status)
        type(spec_t), intent(in) :: spec
        character(len=*), intent(in) :: filename
        integer, intent(out), optional :: status
        integer :: ios

        ios = 0
        if (spec_target_is_json(filename)) then
            call spec_to_json_file(spec, filename, ios)
        else
            call render_spec_to_file(spec, filename, ios)
        end if
        if (present(status)) status = ios
    end subroutine spec_savefig

end module fortplot_spec_builder
