# This file is a part of Julia. License is MIT: https://julialang.org/license

# clipboard copy and paste

"""
    has_system_clipboard() -> Bool

Return `true` if the operating system has a clipboard mechanism available.
On macOS and Windows this is always `true`. On Linux and FreeBSD, it checks
for the presence of `xclip`, `xsel`, or `wl-copy` (from `wl-clipboard`).
On other systems, returns `false`.

!!! compat "Julia 1.14"
    This function requires Julia 1.14 or later.

See also [`clipboard`](@ref).
"""
function has_system_clipboard end

if Sys.isapple()
    has_system_clipboard() = true
    function clipboard(x)
        pbcopy_cmd = `pbcopy`

        # OSX shells, especially when run within `tmux` or `screen`, can be
        # disconnected from the global shell namespace, which causes problems
        # with clipboards.  Luckily, the `reattach-to-user-namespace` utility
        # dodges these issues quite nicely, so we automatically utilize it if
        # it is installed.
        if Sys.which("reattach-to-user-namespace") !== nothing
            pbcopy_cmd = `reattach-to-user-namespace pbcopy`
        end

        open(pipeline(pbcopy_cmd, stderr=stderr), "w") do io
            print(io, x)
        end
        nothing
    end
    function clipboard()
        pbpaste_cmd = `pbpaste`

        # See above comment in `clipboard(x)`
        if Sys.which("reattach-to-user-namespace") !== nothing
            pbpaste_cmd = `reattach-to-user-namespace pbpaste`
        end
        return read(pbpaste_cmd, String)
    end

elseif Sys.islinux() || Sys.KERNEL === :FreeBSD
    function has_system_clipboard()
        try
            clipboardcmd()
            return true
        catch
            return false
        end
    end
    _clipboardcmd = nothing
    const _clipboard_copy = Dict(
            :xsel  => Sys.islinux() ?
                `xsel --input --clipboard` :
                `xsel -c`,
            :xclip => `xclip -silent -in -selection clipboard`,
            :wlclipboard => `wl-copy`
        )
    const _clipboard_paste = Dict(
            :xsel  => Sys.islinux() ?
                `xsel --nodetach --output --clipboard` :
                `xsel -p`,
            :xclip => `xclip -quiet -out -selection clipboard`,
            :wlclipboard => `wl-paste`
        )
    function clipboardcmd()
        global _clipboardcmd
        _clipboardcmd !== nothing && return _clipboardcmd
        for cmd in (:xclip, :xsel, :wlclipboard)
            # wl-clipboard ships wl-copy/paste individually
            c = cmd === :wlclipboard ? Symbol("wl-copy") : cmd
            success(pipeline(`which $c`, devnull)) && return _clipboardcmd = cmd
        end
        pkgs = @static if Sys.KERNEL === :FreeBSD
            "x11/xsel or x11/xclip"
        else
            "xsel or xclip or wl-clipboard"
        end
        error("no clipboard command found, please install $pkgs")
    end
    function clipboard(x)
        c = clipboardcmd()
        cmd = _clipboard_copy[c]
        open(pipeline(cmd, stderr=stderr), "w") do io
            print(io, x)
        end
        nothing
    end
    function clipboard()
        c = clipboardcmd()
        cmd = _clipboard_paste[c]
        return read(pipeline(cmd, stderr=stderr), String)
    end

elseif Sys.iswindows()
    has_system_clipboard() = true
    function clipboard(x::AbstractString)
        if Base.containsnul(x)
            throw(ArgumentError("Windows clipboard strings cannot contain NUL character"))
        end
        x_u16 = Base.cwstring(x)
        pdata = Ref(Ptr{UInt16}(C_NULL))
        function cleanup(cause)
            errno = cause === :success ? UInt32(0) : Libc.GetLastError()
            if cause !== :OpenClipboard
                if cause !== :success && pdata[] != C_NULL
                    ccall((:GlobalFree, "kernel32"), stdcall, Cint, (Ptr{UInt16},), pdata[])
                end
                ccall((:CloseClipboard, "user32"), stdcall, Cint, ()) == 0 && Base.windowserror(:CloseClipboard) # this should never fail
            end
            cause === :success || Base.windowserror(cause, errno)
            nothing
        end
        ccall((:OpenClipboard, "user32"), stdcall, Cint, (Ptr{Cvoid},), C_NULL) == 0 && return Base.windowserror(:OpenClipboard)
        ccall((:EmptyClipboard, "user32"), stdcall, Cint, ()) == 0 && return cleanup(:EmptyClipboard)
        # copy data to locked, allocated space
        pdata[] = ccall((:GlobalAlloc, "kernel32"), stdcall, Ptr{UInt16}, (Cuint, Csize_t), 2 #=GMEM_MOVEABLE=#, sizeof(x_u16))
        pdata[] == C_NULL && return cleanup(:GlobalAlloc)
        plock = ccall((:GlobalLock, "kernel32"), stdcall, Ptr{UInt16}, (Ptr{UInt16},), pdata[])
        plock == C_NULL && return cleanup(:GlobalLock)
        GC.@preserve x_u16 memcpy(plock, Base.unsafe_convert(Ptr{UInt16}, Base.cconvert(Ptr{UInt16}, x_u16)), sizeof(x_u16))
        unlock = ccall((:GlobalUnlock, "kernel32"), stdcall, Cint, (Ptr{UInt16},), pdata[])
        (unlock == 0 && Libc.GetLastError() == 0) || return cleanup(:GlobalUnlock) # this should never fail
        pset = ccall((:SetClipboardData, "user32"), stdcall, Ptr{UInt16}, (Cuint, Ptr{UInt16}), 13, pdata[]) # CF_UNICODETEXT
        pdata[] != pset && return cleanup(:SetClipboardData)
        cleanup(:success)
    end
    clipboard(x) = clipboard(sprint(print, x)::String)
    function clipboard()
        function cleanup(cause)
            errno = cause === :success ? UInt32(0) : Libc.GetLastError()
            if cause !== :OpenClipboard
                ccall((:CloseClipboard, "user32"), stdcall, Cint, ()) == 0 && Base.windowserror(:CloseClipboard) # this should never fail
            end
            if cause !== :success && !(cause === :GetClipboardData && (errno == 0x8004006A || errno == 0x800401D3)) # ignore DV_E_CLIPFORMAT and CLIPBRD_E_BAD_DATA from GetClipboardData
                Base.windowserror(cause, errno)
            end
            ""
        end
        ccall((:OpenClipboard, "user32"), stdcall, Cint, (Ptr{Cvoid},), C_NULL) == 0 && return Base.windowserror(:OpenClipboard)
        ccall(:SetLastError, stdcall, Cvoid, (UInt32,), 0) # allow distinguishing if the clipboard simply didn't have text
        pdata = ccall((:GetClipboardData, "user32"), stdcall, Ptr{UInt16}, (Cuint,), 13) # CF_UNICODETEXT
        pdata == C_NULL && return cleanup(:GetClipboardData)
        plock = ccall((:GlobalLock, "kernel32"), stdcall, Ptr{UInt16}, (Ptr{UInt16},), pdata)
        plock == C_NULL && return cleanup(:GlobalLock)
        s = try
            # find NUL terminator (0x0000 16-bit code unit)
            len = 0
            while unsafe_load(plock, len + 1) != 0
                len += 1
            end
            # get Vector{UInt16}, transcode data to UTF-8, make a String of it
            transcode(String, unsafe_wrap(Array, plock, len))
        finally
           unlock = ccall((:GlobalUnlock, "kernel32"), stdcall, Cint, (Ptr{UInt16},), pdata)
           (unlock != 0 || Libc.GetLastError() == 0) || return cleanup(:GlobalUnlock) # this should never fail
           cleanup(:success)
        end
        return s
    end

else
    has_system_clipboard() = false
    clipboard(x="") = error("`clipboard` function not implemented for $(Sys.KERNEL)")
end


"""
    clipboard(x)

Send a printed form of `x` to the operating system clipboard ("copy").
"""
clipboard(x)

"""
    clipboard()::String

Return a string with the contents of the operating system clipboard ("paste").
"""
clipboard()
