/*
 * Copyright 2019 The Starlark in Rust Authors.
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

//! Utilities to test Starlark code execution, using the [`Assert`] type and top-level functions.
//!
//! There are two general approaches. You can either use the functions in this module directly, e.g.:
//!
//! ```
//! use starlark::assert;
//! assert::eq("1+2", "3");
//! ```
//!
//! Or create an [`Assert`] object, which supports the same assertions, but also let's you modify the
//! environment in which the tests are run, e.g.:
//!
//! ```
//! use starlark::assert::Assert;
//! use starlark::syntax::Dialect;
//!
//! let mut a = Assert::new();
//! a.dialect(&Dialect::Standard); // Use standard Starlark
//! a.eq("1+2", "3");
//! ```
//!
//! The tests in question may be run multiple times, in different modes, to maximise test coverage.
//! For example, execution tests are run at different garbage collection settings. Parsing tests are run
//! with both Unix and Windows newlines.

#[allow(clippy::module_inception)] // This seems a perfectly reasonable thing to do
mod assert;
mod conformance;

pub use assert::*;
pub use conformance::*;


/*
 * Copyright 2019 The Starlark in Rust Authors.
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

use std::marker::PhantomData;
use std::mem;

use dupe::Dupe;

/// String which is `usize` aligned with zeros padding in the end.
#[derive(Copy, Clone, Dupe)]
pub(crate) struct AlignedPaddedStr<'a> {
    /// In bytes.
    len: usize,
    /// Data containing `len` bytes and zero padding in the end.
    data: *const usize,
    _marker: PhantomData<&'a str>,
}

impl<'a> AlignedPaddedStr<'a> {
    #[inline]
    pub(crate) unsafe fn new(len: usize, data: *const usize) -> AlignedPaddedStr<'a> {
        AlignedPaddedStr {
            len,
            data,
            _marker: PhantomData,
        }
    }

    /// Len of string in words.
    #[inline]
    fn len_words(self) -> usize {
        (self.len + mem::size_of::<usize>() - 1) / mem::size_of::<usize>()
    }
}

impl<'a> PartialEq for AlignedPaddedStr<'a> {
    #[inline]
    fn eq(&self, other: &Self) -> bool {
        if self.len != other.len {
            return false;
        }

        // We know strings are aligned, zero-padded and short,
        // so we can do better than generic SIMD-optimized `memcmp`
        // https://rust.godbolt.org/z/cdscb37Yd
        let len_words = self.len_words();
        for i in 0..len_words {
            if unsafe { *self.data.add(i) != *other.data.add(i) } {
                return false;
            }
        }
        true
    }
}


/*
 * Copyright 2019 The Starlark in Rust Authors.
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

use std::alloc::alloc;
use std::alloc::dealloc;
use std::alloc::Layout;
use std::cell::Cell;
use std::cell::RefCell;
use std::mem;
use std::mem::MaybeUninit;
use std::ptr;
use std::ptr::NonNull;
use std::slice;

use crate::collections::maybe_uninit_backport::maybe_uninit_write_slice_cloned;
use crate::hint::likely;
use crate::hint::unlikely;

/// Holds the allocation.
struct Buffer {
    ptr: NonNull<u8>,
    layout: Layout,
}

impl Buffer {
    fn alloc(layout: Layout) -> Buffer {
        let ptr = unsafe { alloc(layout) as *mut u8 };
        let ptr = NonNull::new(ptr).unwrap();
        Buffer { ptr, layout }
    }

    fn ptr(&self) -> *mut u8 {
        self.ptr.as_ptr()
    }

    fn end(&self) -> *mut u8 {
        unsafe { self.ptr.as_ptr().add(self.layout.size()) }
    }

    fn size_words(&self) -> usize {
        self.layout.size() / ALIGN
    }
}

impl Drop for Buffer {
    fn drop(&mut self) {
        unsafe { dealloc(self.ptr.as_ptr().cast(), self.layout) }
    }
}

/// We'd love to use the real `alloca`, but don't want to blow through the stack space,
/// so define our own wrapper.
/// We use a single continuous buffer. When it needs upgrading, we double it and keep the old one around.
pub(crate) struct Alloca {
    // An alternative design would be to bake the <T> into the type, so all allocations are of the same type.
    // Benchmarking that, even if the most optimistic scenario (just reallocating a single element)
    // the performance difference is only 2%, so keep the flexibility of not needing to predeclare the type.
    alloc: Cell<*mut Align>,
    end: Cell<*mut Align>,
    buffers: RefCell<Vec<Buffer>>,
}

const INITIAL_SIZE: usize = 1000000; // ~ 1Mb

type Align = u64;
const ALIGN: usize = mem::size_of::<Align>();

impl Alloca {
    pub fn new() -> Self {
        Self::with_capacity(INITIAL_SIZE)
    }

    pub fn with_capacity(size_bytes: usize) -> Self {
        let size_words = (size_bytes + ALIGN - 1) / ALIGN;
        let layout = Layout::array::<Align>(size_words).unwrap();
        let buffer = Buffer::alloc(layout);
        Self {
            alloc: Cell::new(buffer.ptr().cast()),
            end: Cell::new(buffer.end().cast()),
            buffers: RefCell::new(vec![buffer]),
        }
    }

    fn assert_state(&self) {
        unsafe {
            debug_assert!(self.end.get().offset_from(self.alloc.get()) >= 0);
            debug_assert!(
                self.end.get().offset_from(self.alloc.get()) as usize
                    <= self.buffers.borrow().last().unwrap().size_words()
            );
        }
    }

    #[cold]
    #[inline(never)]
    fn allocate_more(&self, len: usize, one: Layout) {
        let want =
            Layout::from_size_align(one.size().checked_mul(len).unwrap(), one.align()).unwrap();
        assert!(want.align() <= mem::size_of::<Align>());
        let size_words =
            self.buffers.borrow().last().unwrap().size_words() * 2 + want.size() / ALIGN;
        let layout = Layout::array::<Align>(size_words).unwrap();
        let buffer = Buffer::alloc(layout);
        let pointer = buffer.ptr().cast();
        let end = buffer.end().cast();
        self.buffers.borrow_mut().push(buffer);
        self.alloc.set(pointer);
        self.end.set(end);
    }

    /// Convert remaining capacity in words to remaining capacity in `T`.
    #[inline(always)]
    fn rem_in_words_to_rem_in_t<T>(rem_in_words: usize) -> usize {
        assert!(mem::align_of::<T>() <= ALIGN);
        rem_in_words * ALIGN / mem::size_of::<T>()
    }

    #[inline(always)]
    fn len_in_to_to_len_in_words<T>(len: usize) -> usize {
        if mem::size_of::<T>() % ALIGN == 0 {
            // Special case to make common case fast:
            // https://rust.godbolt.org/z/adh3nzdzs
            len * (mem::size_of::<T>() / ALIGN)
        } else {
            // Multiplication does not overflow because we know that `[T; len]`
            // is within the size of the current buffer, which is smaller than `isize::MAX`.
            (len * mem::size_of::<T>() + ALIGN - 1) / ALIGN
        }
    }

    /// Note that the `Drop` for the `T` will not be called. That's safe if there is no `Drop`,
    /// or you call it yourself.
    #[inline(always)]
    pub fn alloca_uninit<T, R>(&self, len: usize, k: impl FnOnce(&mut [MaybeUninit<T>]) -> R) -> R {
        self.assert_state();

        assert!(mem::align_of::<T>() <= ALIGN);

        let mut start = self.alloc.get();

        let rem_words = unsafe { self.end.get().offset_from(start) as usize };
        let rem_in_t = Self::rem_in_words_to_rem_in_t::<T>(rem_words);
        if unlikely(len > rem_in_t) {
            self.allocate_more(len, Layout::new::<T>());
            start = self.alloc.get();
        }

        let size_words = Self::len_in_to_to_len_in_words::<T>(len);

        let stop = start.wrapping_add(size_words);
        let old = start;
        self.alloc.set(stop);
        let data = start as *mut MaybeUninit<T>;
        let slice = unsafe { slice::from_raw_parts_mut(data, len) };
        let res = k(slice);

        // If the pointer changed, it means a callback called alloca again,
        // which allocated a new buffer. So we are abandoning the current allocation here,
        // and new allocations will use the new buffer even if the current buffer has space.
        if likely(self.alloc.get() == stop) {
            self.alloc.set(old);
        }

        self.assert_state();

        res
    }

    #[inline(always)]
    pub fn alloca_init<T, R>(
        &self,
        len: usize,
        mut init: impl FnMut() -> T,
        k: impl FnOnce(&mut [T]) -> R,
    ) -> R {
        self.alloca_uninit(len, |data| {
            for x in data.iter_mut() {
                x.write(init());
            }
            let data = unsafe { &mut *(data as *mut [MaybeUninit<T>] as *mut [T]) };
            k(data)
        })
    }

    #[allow(dead_code)] // Dead, but morally a sensible API to provide, and useful for testing
    #[inline(always)]
    pub fn alloca_fill<T: Copy, R>(&self, len: usize, fill: T, k: impl FnOnce(&mut [T]) -> R) -> R {
        self.alloca_init(len, || fill, k)
    }

    fn alloca_concat_slow<T: Clone, R, F>(&self, x: &[T], y: &[T], k: F) -> R
    where
        F: FnOnce(&[T]) -> R,
    {
        // We clone the input into a slice allocated in `Alloca`,
        // slice does not invoke `Drop`, so we do drop explicitly.
        struct DropSliceGuard<A>(*mut [A]);
        impl<A> Drop for DropSliceGuard<A> {
            fn drop(&mut self) {
                unsafe {
                    ptr::drop_in_place(self.0);
                }
            }
        }

        self.alloca_uninit(x.len() + y.len(), |xy| {
            let (x_uninit, y_uninit) = xy.split_at_mut(x.len());
            let x = maybe_uninit_write_slice_cloned(x_uninit, x);
            let _x_drop_guard = DropSliceGuard(x);
            let y = maybe_uninit_write_slice_cloned(y_uninit, y);
            let _y_drop_guard = DropSliceGuard(y);
            let xy = unsafe { &mut *(xy as *mut [MaybeUninit<T>] as *mut [T]) };
            k(xy)
        })
    }

    /// Concat two slices and invoke the callback with the result.
    /// Use either slice as is if the other is empty,
    /// otherwise clone the elements into a temporary slice.
    #[inline(always)]
    pub(crate) fn alloca_concat<T: Clone, R, F>(&self, x: &[T], y: &[T], k: F) -> R
    where
        F: FnOnce(&[T]) -> R,
    {
        if x.is_empty() {
            k(y)
        } else if y.is_empty() {
            k(x)
        } else {
            self.alloca_concat_slow(x, y, k)
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[allow(clippy::identity_op)]
    #[test]
    fn test_rem_in_words_to_rem_in_t() {
        assert_eq!(10, Alloca::rem_in_words_to_rem_in_t::<Align>(10));
        assert_eq!(
            1 * ALIGN / mem::size_of::<u32>(),
            Alloca::rem_in_words_to_rem_in_t::<u32>(1)
        );
        assert_eq!(
            2 * ALIGN / mem::size_of::<u32>(),
            Alloca::rem_in_words_to_rem_in_t::<u32>(2)
        );
        assert_eq!(1, Alloca::rem_in_words_to_rem_in_t::<[u8; ALIGN - 1]>(1));
        assert_eq!(1, Alloca::rem_in_words_to_rem_in_t::<[u8; ALIGN]>(1));
        assert_eq!(0, Alloca::rem_in_words_to_rem_in_t::<[u8; ALIGN + 1]>(1));
        assert_eq!(2, Alloca::rem_in_words_to_rem_in_t::<[u8; ALIGN - 1]>(2));
        assert_eq!(2, Alloca::rem_in_words_to_rem_in_t::<[u8; ALIGN]>(2));
        assert_eq!(1, Alloca::rem_in_words_to_rem_in_t::<[u8; ALIGN + 1]>(2));
    }

    #[test]
    fn test_len_in_t_to_len_in_words() {
        assert_eq!(10, Alloca::len_in_to_to_len_in_words::<Align>(10));
        assert_eq!(1, Alloca::len_in_to_to_len_in_words::<u8>(1));
        assert_eq!(1, Alloca::len_in_to_to_len_in_words::<u8>(ALIGN - 1));
        assert_eq!(1, Alloca::len_in_to_to_len_in_words::<u8>(ALIGN));
        assert_eq!(2, Alloca::len_in_to_to_len_in_words::<u8>(ALIGN + 1));
        assert_eq!(2, Alloca::len_in_to_to_len_in_words::<u8>(2 * ALIGN - 1));
        assert_eq!(2, Alloca::len_in_to_to_len_in_words::<u8>(2 * ALIGN));
        assert_eq!(3, Alloca::len_in_to_to_len_in_words::<u8>(2 * ALIGN + 1));
        assert_eq!(1, Alloca::len_in_to_to_len_in_words::<[u8; ALIGN - 1]>(1));
        assert_eq!(1, Alloca::len_in_to_to_len_in_words::<[u8; ALIGN]>(1));
        assert_eq!(2, Alloca::len_in_to_to_len_in_words::<[u8; ALIGN + 1]>(1));
        assert_eq!(2, Alloca::len_in_to_to_len_in_words::<[u8; ALIGN - 1]>(2));
        assert_eq!(2, Alloca::len_in_to_to_len_in_words::<[u8; ALIGN]>(2));
        assert_eq!(3, Alloca::len_in_to_to_len_in_words::<[u8; ALIGN + 1]>(2));
    }

    #[test]
    fn test_alloca() {
        // Use a small capacity to encourage overflow behaviour
        let a = Alloca::with_capacity(100);
        a.alloca_fill(3, 8usize, |xs| {
            xs[0] = 5;
            xs[2] = xs[0] + xs[1] + 15;
            a.alloca_fill(200, 18usize, |ys| {
                assert_eq!(xs[2], 8 + 5 + 15);
                assert_eq!(ys[0], 18);
                assert_eq!(ys[200 - 1], 18);
            });
            a.alloca_fill(3, 1u64, |_| {});
            assert_eq!(xs[2], 8 + 5 + 15);
        })
    }

    #[test]
    fn trigger_bug() {
        let a = Alloca::with_capacity(100);
        for _ in 0..100 {
            a.alloca_fill(10, 17usize, |_| {
                a.alloca_fill(1000, 19usize, |_| {});
            });
        }

        assert_eq!(2, a.buffers.borrow().len());
    }

    #[test]
    fn test_alloca_bug_not_aligned() {
        let a = Alloca::with_capacity(100);
        a.alloca_fill(1, 17u8, |xs| {
            // Bug was triggered because the end of first allocation
            // was rounded down instead of up.
            a.alloca_fill(1, 19u8, |ys| {
                assert_eq!([17], xs);
                assert_eq!([19], ys);
            });
        })
    }

    #[test]
    fn test_alloca_concat() {
        let a = Alloca::new();
        let x = vec!["ab".to_owned()];
        let y = vec!["cd".to_owned()];
        a.alloca_concat(&x, &[], |xy| {
            // No copy here.
            assert_eq!(xy.as_ptr(), x.as_ptr());
        });
        a.alloca_concat(&[], &x, |xy| {
            // No copy here.
            assert_eq!(xy.as_ptr(), x.as_ptr());
        });
        a.alloca_concat(&x, &y, |xy| {
            assert_eq!(&["ab".to_owned(), "cd".to_owned()], xy);
        });
    }
}


/*
 * Copyright 2019 The Starlark in Rust Authors.
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

// Copy-paste from std.
#![allow(clippy::needless_range_loop)]

use std::mem;
use std::mem::MaybeUninit;
use std::ptr;

/// Copy-paste `MaybeUninit::write_slice_cloned`. Replace back when stabilized.
pub(crate) fn maybe_uninit_write_slice_cloned<'a, T>(
    this: &'a mut [MaybeUninit<T>],
    src: &[T],
) -> &'a mut [T]
where
    T: Clone,
{
    // unlike copy_from_slice this does not call clone_from_slice on the slice
    // this is because `MaybeUninit<T: Clone>` does not implement Clone.

    struct Guard<'a, T> {
        slice: &'a mut [MaybeUninit<T>],
        initialized: usize,
    }

    impl<'a, T> Drop for Guard<'a, T> {
        fn drop(&mut self) {
            let initialized_part = &mut self.slice[..self.initialized];
            // SAFETY: this raw slice will contain only initialized objects
            // that's why, it is allowed to drop it.
            unsafe {
                ptr::drop_in_place(&mut *(initialized_part as *mut [MaybeUninit<T>] as *mut [T]));
            }
        }
    }

    assert_eq!(
        this.len(),
        src.len(),
        "destination and source slices have different lengths"
    );
    // NOTE: We need to explicitly slice them to the same length
    // for bounds checking to be elided, and the optimizer will
    // generate memcpy for simple cases (for example T = u8).
    let len = this.len();
    let src = &src[..len];

    // guard is needed b/c panic might happen during a clone
    let mut guard = Guard {
        slice: this,
        initialized: 0,
    };

    for i in 0..len {
        guard.slice[i].write(src[i].clone());
        guard.initialized += 1;
    }

    mem::forget(guard);

    // SAFETY: Valid elements have just been written into `this` so it is initialized
    unsafe { &mut *(this as *mut [MaybeUninit<T>] as *mut [T]) }
}

/// Copy-paste `MaybeUninit::write_slice`. Replace back when stabilized.
pub(crate) fn maybe_uninit_write_slice<'a, T>(
    this: &'a mut [MaybeUninit<T>],
    src: &[T],
) -> &'a mut [T]
where
    T: Copy,
{
    // SAFETY: &[T] and &[MaybeUninit<T>] have the same layout
    let uninit_src: &[MaybeUninit<T>] = unsafe { mem::transmute(src) };

    this.copy_from_slice(uninit_src);

    // SAFETY: Valid elements have just been copied into `this` so it is initialized
    unsafe { &mut *(this as *mut [MaybeUninit<T>] as *mut [T]) }
}


/*
 * Copyright 2019 The Starlark in Rust Authors.
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

//! Defines [`SmallMap`] and [`SmallSet`] - collections with deterministic iteration and small memory footprint.
//!
//! These structures use vector backed storage if there are only a few elements, and and index
//! for larger collections. The API mirrors standard Rust collections.

pub use starlark_map::small_map::IntoIter;
pub use starlark_map::small_map::Iter;
pub use starlark_map::small_map::IterMut;
pub use starlark_map::small_map::SmallMap;
pub use starlark_map::small_set::SmallSet;
pub use starlark_map::Equivalent;
pub use starlark_map::Hashed;
pub use starlark_map::StarlarkHashValue;
pub use starlark_map::StarlarkHasher;

pub(crate) mod aligned_padded_str;
pub(crate) mod alloca;
pub(crate) mod maybe_uninit_backport;
pub(crate) mod string_pool;
pub(crate) mod symbol_map;


/*
 * Copyright 2019 The Starlark in Rust Authors.
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

//! Reuse string allocation.

/// Pool of strings.
#[derive(Default, Debug)]
pub(crate) struct StringPool {
    /// Empty strings with (typically) non-zero capacity.
    strings: Vec<String>,
}

impl StringPool {
    /// Fetch a string from the pool or create an empty one.
    ///
    /// It is OK to not return a string to the pool.
    pub(crate) fn alloc(&mut self) -> String {
        let string = self.strings.pop().unwrap_or_default();
        debug_assert!(string.is_empty());
        string
    }

    /// Return the string back to the pool.
    ///
    /// Only strings previously allocated with this pool should be returned,
    /// otherwise pool may grow too much.
    pub(crate) fn release(&mut self, mut s: String) {
        s.clear();
        self.strings.push(s);
    }
}


/*
 * Copyright 2019 The Starlark in Rust Authors.
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

//! An optimised string HashMap which goes even faster when the keys can
//! be pre-hashed or otherwise precomputed.
//!
//! The two bottlenecks in our use of these hash tables are computing the hashes and comparing
//! the resulting keys for equality. We precompute the hashes. We also use `[usize]` to do faster
//! comparison when possible. We use the Starlark SmallHash hashes, promoted by IdHasher,
//! so we can reuse a SmallMap hash.
//!
//! Benchmarks on which the `[usize]` choice was made (mac/linux, all in ns):
//!                           8 bytes       32 bytes      64 bytes
//! slice equality (memcmp)   3.5/3.8       3.5/ 3.0      4.5/ 4.7
//! usize equality loop       1.0/1.4       2.7/ 3.5      3.5/ 6.0
//! u8 equality loop          3.4/5.7      13.7/19.7     22.6/44.8
//!
//! Measuring some sample strings, the P50 = 21 bytes, P75 = 27, P95 = 35,
//! so we can reasonably expect to hit the smaller cases most often.

use std::fmt;
use std::fmt::Debug;
use std::intrinsics::copy_nonoverlapping;
use std::mem;
use std::slice;
use std::str;

use allocative::Allocative;
use hashbrown::raw::RawTable;

use crate as starlark;
use crate::coerce::Coerce;
use crate::collections::Hashed;
use crate::collections::StarlarkHashValue;
use crate::values::StringValue;
use crate::values::Trace;

// We use a RawTable (the thing that underlies HashMap) so we can look up efficiently
// and easily by Symbol and str, without being limited by `Borrow` traits.
#[derive(Clone, Trace, Allocative)]
pub(crate) struct SymbolMap<T>(RawTable<(Symbol, T)>);

impl<T: Debug> Debug for SymbolMap<T> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_map()
            .entries(self.iter().map(|x| (&x.0, &x.1)))
            .finish()
    }
}

/// A pre-hashed string used for efficient dictionary lookup.
#[derive(Clone, Trace, Allocative)]
pub(crate) struct Symbol {
    hash: u64,
    len: u32,
    payload: Box<[usize]>,
    small_hash: StarlarkHashValue,
}

unsafe impl Coerce<Symbol> for Symbol {}

impl Debug for Symbol {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.as_str().fmt(f)
    }
}

impl PartialEq for Symbol {
    fn eq(&self, other: &Self) -> bool {
        if self.len != other.len {
            return false;
        }

        let p1 = &*self.payload;
        let p2 = &*other.payload;
        // Important to use the payload len, which is in u64 units, rather than len which is in u8
        for i in 0..self.payload.len() {
            // Safe because we checked the lengths at the start
            if unsafe { p1.get_unchecked(i) != p2.get_unchecked(i) } {
                return false;
            }
        }
        true
    }
}

impl Eq for Symbol {}

impl Symbol {
    pub fn new(x: &str) -> Self {
        Self::new_hashed(Hashed::new(x))
    }

    pub fn new_hashed(x: Hashed<&str>) -> Self {
        let small_hash = x.hash();
        let hash = small_hash.promote();
        let len = x.key().len();
        let len_words = (len + mem::size_of::<usize>() - 1) / mem::size_of::<usize>();
        let mut payload = vec![0; len_words]; // 0 pad it at the end
        unsafe {
            copy_nonoverlapping(x.key().as_ptr(), payload.as_mut_ptr() as *mut u8, len);
        }
        Self {
            hash,
            len: len.try_into().unwrap(),
            payload: payload.into_boxed_slice(),
            small_hash,
        }
    }

    pub fn as_str(&self) -> &str {
        // All safe because we promise we started out with a str
        unsafe {
            let s = slice::from_raw_parts(
                self.payload.as_ptr() as *const usize as *const u8,
                self.len as usize,
            );
            str::from_utf8_unchecked(s)
        }
    }

    pub(crate) fn as_str_hashed(&self) -> Hashed<&str> {
        Hashed::new_unchecked(self.small_hash, self.as_str())
    }

    pub fn small_hash(&self) -> StarlarkHashValue {
        self.small_hash
    }
}

impl<T> SymbolMap<T> {
    pub fn new() -> Self {
        Self(RawTable::new())
    }

    pub fn with_capacity(capacity: usize) -> Self {
        Self(RawTable::with_capacity(capacity))
    }

    pub fn insert(&mut self, key: &str, value: T) -> Option<T> {
        let s = Symbol::new(key);
        if let Some((_, item)) = self.0.get_mut(s.hash, |x| s == x.0) {
            Some(mem::replace(item, value))
        } else {
            // This insert doesn't remove old values, so do that manually first
            self.0.insert(s.hash, (s, value), |x| x.0.hash);
            None
        }
    }

    pub fn get(&self, key: &Symbol) -> Option<&T> {
        self.0.get(key.hash, |x| key == &x.0).map(|x| &x.1)
    }

    pub fn get_str(&self, key: &str) -> Option<&T> {
        self.get_hashed_str(Hashed::new(key))
    }

    pub fn get_hashed_str(&self, key: Hashed<&str>) -> Option<&T> {
        self.0
            .get(key.hash().promote(), |x| x.0.as_str() == *key.key())
            .map(|x| &x.1)
    }

    pub(crate) fn get_hashed_string_value(&self, key: Hashed<StringValue>) -> Option<&T> {
        self.0
            .get(key.hash().promote(), |x| x.0.as_str() == key.key().as_str())
            .map(|x| &x.1)
    }

    pub fn len(&self) -> usize {
        self.0.len()
    }

    pub fn iter<'a>(&'a self) -> impl ExactSizeIterator<Item = &'a (Symbol, T)> + 'a {
        // Unsafe because it doesn't have a lifetime, but we added one in the type signature
        unsafe { self.0.iter().map(|x| x.as_ref()) }
    }

    pub fn keys<'a>(&'a self) -> impl ExactSizeIterator<Item = &'a Symbol> + 'a {
        self.iter().map(|x| &x.0)
    }

    pub fn values<'a>(&'a self) -> impl ExactSizeIterator<Item = &'a T> + 'a {
        self.iter().map(|x| &x.1)
    }
}


/*
 * Copyright 2019 The Starlark in Rust Authors.
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

use crate::collections::SmallMap;
use crate::debug::inspect::to_scope_names_by_local_slot_id;
use crate::eval::runtime::slots::LocalSlotIdCapturedOrNot;
use crate::eval::Evaluator;
use crate::syntax::AstModule;
use crate::values::FrozenStringValue;
use crate::values::Value;

impl<'v, 'a> Evaluator<'v, 'a> {
    /// Evaluate statements in the existing context. This function is designed for debugging,
    /// not production use.
    ///
    /// There are lots of health warnings on this code. Might not work with frozen modules, unassigned variables,
    /// nested definitions etc. It would be a bad idea to rely on the results of continued execution
    /// after evaluating stuff randomly.
    pub fn eval_statements(&mut self, statements: AstModule) -> anyhow::Result<Value<'v>> {
        // We are doing a lot of funky stuff here. It's amazing anything works, so let's not push our luck with GC.
        self.disable_gc();

        // Everything must be evaluated with the current heap (or we'll lose memory), which means
        // the current module (eval.module_env).
        // We also want access to the module variables (fine), the locals (need to move them over),
        // and the frozen variables (move them over).
        // Afterwards, we want to put everything back - locals can move back to locals, modules
        // can stay where they are, but frozen values are discarded.

        // We want all the local variables to be available to the module, so we capture
        // everything before, shove the local variables into the module, and then revert after
        let original_module: SmallMap<FrozenStringValue, Option<Value<'v>>> = self
            .module_env
            .mutable_names()
            .all_names_and_slots()
            .into_iter()
            .map(|(name, slot)| (name, self.module_env.slots().get_slot(slot)))
            .collect();

        // Push all the frozen variables into the module
        if let Some(frozen) = &self.module_variables {
            for (name, slot) in frozen.names.symbols() {
                if let Some(value) = frozen.get_slot(slot) {
                    self.module_env.set(&name, value.to_value())
                }
            }
        }

        // Push all local variables into the module
        let locals = self
            .call_stack
            .to_function_values()
            .into_iter()
            .rev()
            .find_map(to_scope_names_by_local_slot_id);
        if let Some(names) = &locals {
            for (slot, name) in names.iter().enumerate() {
                if let Some(value) = self
                    .current_frame
                    .get_slot_slow(LocalSlotIdCapturedOrNot(slot as u32))
                {
                    self.module_env.set(name, value)
                }
            }
        }

        let orig_module_variables = self.module_variables.take();
        let globals = self.top_frame_def_info_for_debugger()?.globals;
        let res = self.eval_module(statements, &globals);
        self.module_variables = orig_module_variables;

        // Now put the Module back how it was before we started, as best we can
        // and move things into locals if that makes sense
        if let Some(names) = &locals {
            for (slot, name) in names.iter().enumerate() {
                if let Some(value) = self.module_env.get(name) {
                    self.current_frame
                        .set_slot_slow(LocalSlotIdCapturedOrNot(slot as u32), value)
                }
            }
            for (name, slot) in self.module_env.mutable_names().all_names_and_slots() {
                match original_module.get(&name) {
                    None => self.module_env.mutable_names().hide_name(&name),
                    Some(Some(value)) => self.module_env.slots().set_slot(slot, *value),
                    _ => {} // No way to unassign a previously assigned value yet
                }
            }
        }

        res
    }
}

#[cfg(test)]
mod tests {
    use itertools::Itertools;
    use starlark_derive::starlark_module;

    use super::*;
    use crate as starlark;
    use crate::assert;
    use crate::environment::GlobalsBuilder;
    use crate::syntax::Dialect;
    use crate::wasm::is_wasm;

    #[starlark_module]
    fn debugger(builder: &mut GlobalsBuilder) {
        fn debug_evaluate<'v>(
            code: String,
            eval: &mut Evaluator<'v, '_>,
        ) -> anyhow::Result<Value<'v>> {
            let ast = AstModule::parse("interactive", code, &Dialect::Extended)?;
            eval.eval_statements(ast)
        }
    }

    #[test]
    fn test_debug_evaluate() {
        if is_wasm() {
            return;
        }

        let mut a = assert::Assert::new();
        a.globals_add(debugger);
        let check = r#"
assert_eq(debug_evaluate("1+2"), 3)
x = 10
assert_eq(debug_evaluate("x"), 10)
assert_eq(debug_evaluate("x = 5"), None)
assert_eq(x, 5)
y = [20]
debug_evaluate("y.append(30)")
assert_eq(y, [20, 30])
"#;
        // Check evaluation works at the root
        a.pass(check);
        // And inside functions
        a.pass(&format!(
            "def local():\n{}\nlocal()",
            check.lines().map(|x| format!("    {}", x)).join("\n")
        ));

        // Check we get the right stack frames
        a.pass(
            r#"
def foo(x, y, z):
    return bar(y)
def bar(x):
    return debug_evaluate("x")
assert_eq(foo(1, 2, 3), 2)
"#,
        );

        // Check we can access module-level and globals
        a.pass(
            r#"
x = 7
def bar(y):
    return debug_evaluate("x + y")
assert_eq(bar(4), 4 + 7)
"#,
        );

        // Check module-level access works in imported modules
        a.module(
            "test",
            r#"
x = 7
z = 2
def bar(y):
    assert_eq(x, 7)
    debug_evaluate("x = 20")
    assert_eq(x, 7) # doesn't work for frozen variables
    return debug_evaluate("x + y + z")
"#,
        );
        a.pass("load('test', 'bar'); assert_eq(bar(4), 4 + 7 + 2)");
    }
}


/*
 * Copyright 2019 The Starlark in Rust Authors.
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

use crate::collections::SmallMap;
use crate::eval::compiler::def::Def;
use crate::eval::compiler::def::FrozenDef;
use crate::eval::runtime::slots::LocalSlotIdCapturedOrNot;
use crate::eval::Evaluator;
use crate::values::FrozenStringValue;
use crate::values::Value;
use crate::values::ValueLike;

pub(crate) fn to_scope_names_by_local_slot_id<'v>(x: Value<'v>) -> Option<&'v [FrozenStringValue]> {
    if x.unpack_frozen().is_some() {
        x.downcast_ref::<FrozenDef>()
            .map(|x| x.def_info.used.as_ref())
    } else {
        x.downcast_ref::<Def>().map(|x| x.def_info.used.as_ref())
    }
}

impl<'v, 'a> Evaluator<'v, 'a> {
    /// Obtain the local variables currently in scope. When at top-level these will be
    /// [`Module`](crate::environment::Module) variables, otherwise local definitions. The precise number of variables
    /// may change over time due to optimisation. The only legitimate use of this function is for debugging.
    pub fn local_variables(&self) -> SmallMap<String, Value<'v>> {
        inspect_local_variables(self).unwrap_or_else(|| inspect_module_variables(self))
    }
}

fn inspect_local_variables<'v>(eval: &Evaluator<'v, '_>) -> Option<SmallMap<String, Value<'v>>> {
    // First we find the first entry on the call_stack which contains a Def (and thus has locals)
    let xs = eval.call_stack.to_function_values();
    let names = xs
        .into_iter()
        .rev()
        .find_map(to_scope_names_by_local_slot_id)?;
    let mut res = SmallMap::new();
    for (slot, name) in names.iter().enumerate() {
        // TODO(nga): correctly handle captured.
        if let Some(v) = eval
            .current_frame
            .get_slot_slow(LocalSlotIdCapturedOrNot(slot as u32))
        {
            res.insert(name.as_str().to_owned(), v);
        }
    }
    Some(res)
}

fn inspect_module_variables<'v>(eval: &Evaluator<'v, '_>) -> SmallMap<String, Value<'v>> {
    let mut res = SmallMap::new();
    for (name, slot) in eval.module_env.mutable_names().all_names_and_slots() {
        if let Some(v) = eval.module_env.slots().get_slot(slot) {
            res.insert(name.as_str().to_owned(), v);
        }
    }
    res
}

#[cfg(test)]
mod tests {
    use starlark_derive::starlark_module;

    use crate as starlark;
    use crate::assert;
    use crate::coerce::coerce;
    use crate::collections::SmallMap;
    use crate::environment::GlobalsBuilder;
    use crate::eval::Evaluator;
    use crate::slice_vec_ext::SliceExt;
    use crate::values::dict::Dict;

    #[starlark_module]
    fn debugger(builder: &mut GlobalsBuilder) {
        fn debug_inspect_stack(eval: &mut Evaluator) -> anyhow::Result<Vec<String>> {
            Ok(eval.call_stack().into_frames().map(ToString::to_string))
        }

        fn debug_inspect_variables<'v>(eval: &mut Evaluator<'v, '_>) -> anyhow::Result<Dict<'v>> {
            let mut sm = SmallMap::new();
            for (k, v) in eval.local_variables() {
                sm.insert_hashed(eval.heap().alloc_str(&k).get_hashed(), v);
            }
            Ok(Dict::new(coerce(sm)))
        }
    }

    #[test]
    fn test_debug_stack() {
        let mut a = assert::Assert::new();
        a.globals_add(debugger);
        a.pass(
            r#"
def assert_stack(want):
    stack = debug_inspect_stack()
    assert_eq([x.split(' ')[0] for x in stack[:-2]], want)

assert_stack([])

def f(): assert_stack(["g", "f"])
def g(): f()
g()
"#,
        );
    }

    #[test]
    fn test_debug_variables() {
        let mut a = assert::Assert::new();
        a.globals_add(debugger);
        a.pass(
            r#"
root = 12
_ignore = [x for x in [True]]
def f(x = 1, y = "test"):
    z = x + 5
    for _magic in [False, True]:
        continue
    assert_eq(debug_inspect_variables(), {"x": 1, "y": "hello", "z": 6, "_magic": True})
f(y = "hello")
assert_eq(debug_inspect_variables(), {"root": 12, "f": f, "_ignore": [True]})
"#,
        );
    }
}


/*
 * Copyright 2019 The Starlark in Rust Authors.
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

//! Provides debug-related functionality and utilities.
mod adapter;

mod evaluate;
mod inspect;

pub use adapter::*;


/*
 * Copyright 2019 The Starlark in Rust Authors.
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

use std::collections::HashMap;
use std::fmt::Debug;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
use std::sync::mpsc::channel;
use std::sync::mpsc::Receiver;
use std::sync::mpsc::Sender;
use std::sync::Arc;
use std::sync::Mutex;

use debugserver_types::*;
use dupe::Dupe;

use crate::codemap::FileSpan;
use crate::codemap::FileSpanRef;
use crate::codemap::Span;
use crate::debug::adapter::Breakpoint;
use crate::debug::adapter::ResolvedBreakpoints;
use crate::debug::DapAdapter;
use crate::debug::DapAdapterClient;
use crate::debug::DapAdapterEvalHook;
use crate::debug::ScopesInfo;
use crate::debug::StepKind;
use crate::debug::Variable;
use crate::debug::VariablesInfo;
use crate::eval::BeforeStmtFuncDyn;
use crate::eval::Evaluator;
use crate::slice_vec_ext::SliceExt;
use crate::syntax::AstModule;
use crate::syntax::Dialect;
use crate::values::Value;

pub(crate) fn prepare_dap_adapter(
    client: Box<dyn DapAdapterClient>,
) -> (impl DapAdapter, impl DapAdapterEvalHook) {
    let (sender, receiver) = std::sync::mpsc::channel::<ToEvalMessage>();
    let state = Arc::new(SharedAdapterState {
        client,
        breakpoints: Arc::new(Mutex::new(BreakpointConfig::new())),
        disable_breakpoints: Arc::new(0usize.into()),
    });

    (
        DapAdapterImpl {
            state: state.clone(),
            sender,
        },
        DapAdapterEvalHookImpl::new(state, receiver),
    )
}

type ToEvalMessage = Box<dyn Fn(FileSpanRef, &mut Evaluator) -> Next + Send>;

/// The DapAdapter allows
#[derive(Debug)]
struct DapAdapterImpl {
    state: Arc<SharedAdapterState>,
    sender: Sender<ToEvalMessage>,
}

struct DapAdapterEvalHookImpl {
    state: Arc<SharedAdapterState>,
    receiver: Receiver<ToEvalMessage>,
    step: Option<(StepKind, usize)>,
}

fn evaluate_expr<'v>(
    state: &SharedAdapterState,
    eval: &mut Evaluator<'v, '_>,
    expr: String,
) -> anyhow::Result<Value<'v>> {
    // We don't want to trigger breakpoints during an evaluate,
    // not least because we currently don't allow reenterant evaluate
    state.disable_breakpoints.fetch_add(1, Ordering::SeqCst);
    // Don't use `?`, we need to reset disable_breakpoints.
    let ast = AstModule::parse("interactive", expr, &Dialect::Extended);
    let res = ast.and_then(|ast| eval.eval_statements(ast));
    state.disable_breakpoints.fetch_sub(1, Ordering::SeqCst);
    res
}

impl<'a> BeforeStmtFuncDyn<'a> for DapAdapterEvalHookImpl {
    fn call<'v>(&mut self, span_loc: FileSpanRef, eval: &mut Evaluator<'v, 'a>) {
        let stop = if self.state.disable_breakpoints.load(Ordering::SeqCst) > 0 {
            false
        } else {
            let breaks = self.state.breakpoints.lock().unwrap();
            let breakpoint = breaks.at(span_loc);
            match breakpoint {
                Some(Breakpoint {
                    condition: Some(condition),
                    ..
                }) => match evaluate_expr(&self.state, eval, condition.to_owned()) {
                    Ok(v) => v.to_bool(),
                    _ => true,
                },
                Some(..) => true,
                None => false,
            }
        };

        let step_stop = match self.step {
            None => false,
            Some((StepKind::Into, _)) => true,
            // These aren't quite right because we only get called before statements and so we could
            // return from the current function and be in an expression that then calls another function
            // without hitting a new statement in the outer function.
            Some((StepKind::Over, stack_size)) => eval.call_stack_count() <= stack_size,
            Some((StepKind::Out, stack_size)) => eval.call_stack_count() < stack_size,
        };

        if stop || step_stop {
            self.step = None;
            self.state.client.event_stopped();
            loop {
                let msg = self.receiver.recv();
                match msg.map(|msg| msg(span_loc, eval)) {
                    Ok(Next::Continue) => break,
                    Ok(Next::Step(kind)) => {
                        self.step = Some((kind, eval.call_stack_count()));
                        break;
                    }
                    Ok(Next::RemainPaused) => continue,
                    Err(..) => {
                        // DapAdapter has been dropped so we'll continue.
                        break;
                    }
                }
            }
        }
    }
}

impl Debug for DapAdapterEvalHookImpl {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("DapAdapterEvaluationWrapper").finish()
    }
}

impl DapAdapterEvalHookImpl {
    fn new(state: Arc<SharedAdapterState>, receiver: Receiver<ToEvalMessage>) -> Self {
        Self {
            state,
            receiver,
            step: None,
        }
    }
}

impl DapAdapterEvalHook for DapAdapterEvalHookImpl {
    fn add_dap_hooks<'v, 'a>(self: Box<Self>, eval: &mut Evaluator<'v, 'a>) {
        eval.before_stmt_for_dap((self as Box<dyn BeforeStmtFuncDyn>).into());
    }
}

#[derive(Debug)]
struct BreakpointConfig {
    // maps a source filename to the breakpoint spans for the file
    breakpoints: HashMap<String, HashMap<Span, Breakpoint>>,
}

impl BreakpointConfig {
    fn new() -> Self {
        Self {
            breakpoints: HashMap::new(),
        }
    }

    fn at(&self, span_loc: FileSpanRef) -> Option<&Breakpoint> {
        self.breakpoints
            .get(span_loc.filename())
            .and_then(|file_breaks| file_breaks.get(&span_loc.span))
    }

    fn set_breakpoints(
        &mut self,
        source: &str,
        breakpoints: &ResolvedBreakpoints,
    ) -> anyhow::Result<()> {
        if breakpoints.0.is_empty() {
            self.breakpoints.remove(source);
        } else {
            self.breakpoints.insert(
                source.to_owned(),
                breakpoints
                    .0
                    .iter()
                    .filter_map(|x| x.clone())
                    .map(|x| (x.span.span, x))
                    .collect(),
            );
        }
        Ok(())
    }
}

#[derive(Debug)]
struct SharedAdapterState {
    client: Box<dyn DapAdapterClient>,
    // These breakpoints must all match statements as per before_stmt.
    // Those values for which we abort the execution.
    breakpoints: Arc<Mutex<BreakpointConfig>>,
    // Set while we are doing evaluate calls (>= 1 means disable)
    disable_breakpoints: Arc<AtomicUsize>,
}

#[derive(Debug, Clone, Copy, Dupe)]
enum Next {
    Continue,
    RemainPaused,
    Step(StepKind),
}

fn convert_frame(id: usize, name: String, location: Option<FileSpan>) -> StackFrame {
    let mut s = StackFrame {
        id: id as i64,
        name,
        column: 0,
        line: 0,
        end_column: None,
        end_line: None,
        module_id: None,
        presentation_hint: None,
        source: None,
    };
    if let Some(loc) = location {
        let span = loc.resolve_span();
        s.line = span.begin_line as i64 + 1;
        s.column = span.begin_column as i64 + 1;
        s.end_line = Some(span.end_line as i64 + 1);
        s.end_column = Some(span.end_column as i64 + 1);
        s.source = Some(Source {
            path: Some(loc.filename().to_owned()),
            ..Source::default()
        })
    }
    s
}

impl DapAdapter for DapAdapterImpl {
    fn set_breakpoints(
        &self,
        source: &str,
        breakpoints: &ResolvedBreakpoints,
    ) -> anyhow::Result<()> {
        self.state
            .breakpoints
            .lock()
            .unwrap()
            .set_breakpoints(source, breakpoints)
    }

    fn top_frame(&self) -> anyhow::Result<Option<StackFrame>> {
        self.with_ctx(Box::new(|span, eval| {
            let frame = eval.call_stack_top_frame();
            let name = frame.map_or("".to_owned(), |v| v.name);
            Ok(Some(convert_frame(0, name, Some(span.to_file_span()))))
        }))
    }

    fn stack_trace(&self, _: StackTraceArguments) -> anyhow::Result<StackTraceResponseBody> {
        // Our model of a Frame and the debugger model are a bit different.
        // We record the location of the call, but DAP wants the location we are at.
        // We also have them in the wrong order
        self.with_ctx(Box::new(|span, eval| {
            let frames = eval.call_stack().into_frames();
            let mut next = Some(span.to_file_span());
            let mut res = Vec::with_capacity(frames.len() + 1);
            for (i, x) in frames.iter().rev().enumerate() {
                res.push(convert_frame(i, x.name.clone(), next));
                next = x.location.dupe();
            }
            res.push(convert_frame(frames.len(), "Root".to_owned(), next));
            Ok(StackTraceResponseBody {
                total_frames: Some(res.len() as i64),
                stack_frames: res,
            })
        }))
    }

    fn scopes(&self) -> anyhow::Result<ScopesInfo> {
        self.with_ctx(Box::new(|_, eval| {
            let vars = eval.local_variables();
            Ok(ScopesInfo {
                num_locals: vars.len(),
            })
        }))
    }

    fn variables(&self) -> anyhow::Result<VariablesInfo> {
        self.with_ctx(Box::new(|_, eval| {
            let vars = eval.local_variables();
            Ok(VariablesInfo {
                locals: vars
                    .into_iter()
                    .map(|(name, value)| Variable {
                        name,
                        value: value.to_string(),
                        type_: value.get_type().to_owned(),
                    })
                    .collect(),
            })
        }))
    }

    fn continue_(&self) -> anyhow::Result<()> {
        self.inject_next(Next::Continue);
        Ok(())
    }

    fn step(&self, kind: StepKind) -> anyhow::Result<()> {
        self.inject_next(Next::Step(kind));
        Ok(())
    }

    fn evaluate(&self, expr: &str) -> anyhow::Result<EvaluateResponseBody> {
        let state = self.state.dupe();
        let expression = expr.to_owned();
        self.with_ctx(Box::new(move |_, eval| {
            let s = match evaluate_expr(&state, eval, expression.clone()) {
                Err(e) => format!("{:#}", e),
                Ok(v) => v.to_string(),
            };
            Ok(EvaluateResponseBody {
                indexed_variables: None,
                named_variables: None,
                presentation_hint: None,
                result: s,
                type_: None,
                variables_reference: 0.0,
            })
        }))
    }
}

impl DapAdapterImpl {
    fn inject<T: 'static + Send>(
        &self,
        f: Box<dyn Fn(FileSpanRef, &mut Evaluator) -> (Next, T) + Send>,
    ) -> T {
        let (sender, receiver) = channel();
        self.sender
            .send(Box::new(move |span, eval| {
                let (next, res) = f(span, eval);
                sender.send(res).unwrap();
                next
            }))
            .unwrap();
        receiver.recv().unwrap()
    }

    fn inject_next(&self, next: Next) {
        self.inject(Box::new(move |_, _| (next, ())))
    }

    fn with_ctx<T: 'static + Send>(
        &self,
        f: Box<dyn Fn(FileSpanRef, &mut Evaluator) -> T + Send>,
    ) -> T {
        self.inject(Box::new(move |span, eval| {
            (Next::RemainPaused, f(span, eval))
        }))
    }
}

pub(crate) fn breakpoint(verified: bool) -> debugserver_types::Breakpoint {
    debugserver_types::Breakpoint {
        column: None,
        end_column: None,
        end_line: None,
        id: None,
        line: None,
        message: None,
        source: None,
        verified,
    }
}

pub(crate) fn resolve_breakpoints(
    args: &SetBreakpointsArguments,
    ast: &AstModule,
) -> anyhow::Result<ResolvedBreakpoints> {
    let poss: HashMap<usize, FileSpan> = ast
        .stmt_locations()
        .iter()
        .map(|span| (span.resolve_span().begin_line, span.dupe()))
        .collect();
    Ok(ResolvedBreakpoints(args.breakpoints.as_ref().map_or(
        Vec::new(),
        |v| {
            v.map(|x| {
                poss.get(&(x.line as usize - 1)).map(|span| Breakpoint {
                    span: span.clone(),
                    condition: x.condition.clone(),
                })
            })
        },
    )))
}

pub(crate) fn resolved_breakpoints_to_dap(
    breakpoints: &ResolvedBreakpoints,
) -> SetBreakpointsResponseBody {
    SetBreakpointsResponseBody {
        breakpoints: breakpoints.0.map(|x| breakpoint(x.is_some())),
    }
}


/*
 * Copyright 2019 The Starlark in Rust Authors.
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

//! Provides utilities useful for implementation of the debug adapter protocol (DAP, see
//! <https://microsoft.github.io/debug-adapter-protocol/>), primarily the DapAdapter/DapAdapterEvalHook
//! that provide for debugging a starlark Evaluation.

use std::fmt::Debug;

use debugserver_types::*;
use dupe::Dupe;

use crate::codemap::FileSpan;
use crate::eval::Evaluator;
use crate::syntax::AstModule;

mod implementation;
mod tests;

/// The DapAdapterClient is implemented by the user and provides functionality required by the DapAdapter.
pub trait DapAdapterClient: Debug + Send + Sync + 'static {
    /// Indicates that the evaluation stopped at a breakpoint.
    fn event_stopped(&self);
}

/// Information about the variables scopes
pub struct ScopesInfo {
    /// Number of local variables.
    pub num_locals: usize,
}

/// Information about a variable.
pub struct Variable {
    /// Name of the variable.
    pub name: String,
    /// The value as a String.
    pub value: String,
    /// The variables type.
    pub type_: String,
}

impl Variable {
    /// Helper to convert to the DAP Variable type.
    pub fn to_dap(self) -> debugserver_types::Variable {
        debugserver_types::Variable {
            name: self.name,
            value: self.value,
            type_: Some(self.type_),
            evaluate_name: None,
            indexed_variables: None,
            named_variables: None,
            presentation_hint: None,
            variables_reference: 0,
        }
    }
}

/// The kind of debugger step, used for next/stepin/stepout requests.
#[derive(Debug, Clone, Dupe, Copy)]
pub enum StepKind {
    /// Step "into" the statement. This is generally used on a function call to stop in the
    /// function call. In practice, this will stop on the next statement.
    Into,
    /// Step "over" the statement. This will stop on the next statement in the current function
    /// after the current one (so will step "over" a function call).
    Over,
    /// Step "out" of the current function. This will stop on the next statement after this
    /// function returns.
    Out,
}

/// Information about variables in scope.
pub struct VariablesInfo {
    /// Local variables.
    pub locals: Vec<Variable>,
}

/// The DapAdapter accepts DAP requests and updates the hooks in the running evaluator.
pub trait DapAdapter: Debug + Send + 'static {
    /// Sets multiple breakpoints for a file (and clears existing ones).
    ///
    /// See <https://microsoft.github.io/debug-adapter-protocol/specification#Requests_SetBreakpoints>
    fn set_breakpoints(
        &self,
        source: &str,
        breakpoints: &ResolvedBreakpoints,
    ) -> anyhow::Result<()>;

    /// Gets the top stack frame, may be None if entered from native.
    fn top_frame(&self) -> anyhow::Result<Option<StackFrame>>;

    /// Gets a stacktrace from the current execution state.
    ///
    /// See <https://microsoft.github.io/debug-adapter-protocol/specification#Requests_StackTrace>
    fn stack_trace(&self, args: StackTraceArguments) -> anyhow::Result<StackTraceResponseBody>;

    /// Gets the variables scope for a frame.
    ///
    /// See <https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Scopes>
    fn scopes(&self) -> anyhow::Result<ScopesInfo>;

    /// Gets child variables for a variable reference.
    ///
    /// See <https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Variables>
    fn variables(&self) -> anyhow::Result<VariablesInfo>;

    /// Resumes execution.
    ///
    /// See <https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Continue>
    fn continue_(&self) -> anyhow::Result<()>;

    /// Continues execution until some condition.
    ///
    /// See <https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Next>
    /// <https://microsoft.github.io/debug-adapter-protocol/specification#Requests_StepIn>
    /// <https://microsoft.github.io/debug-adapter-protocol/specification#Requests_StepOut>
    fn step(&self, kind: StepKind) -> anyhow::Result<()>;
    /// Evaluates in expression in the context of the top-most frame.
    ///
    /// See <https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Evaluate>
    fn evaluate(&self, expr: &str) -> anyhow::Result<EvaluateResponseBody>;
}

#[derive(Debug, Clone, Hash, Eq, PartialEq)]
pub struct Breakpoint {
    span: FileSpan,
    condition: Option<String>,
}

/// Breakpoints resolved to their spans.
#[derive(Debug)]
pub struct ResolvedBreakpoints(Vec<Option<Breakpoint>>);

impl ResolvedBreakpoints {
    /// Converts resolved breakpoints to a SetBreakpointsResponseBody. The breakpoints should've been resolved from the corresponding SetBreakpointsRequest.
    pub fn to_response(&self) -> SetBreakpointsResponseBody {
        implementation::resolved_breakpoints_to_dap(self)
    }
}

/// Resolves the breakpoints to their FileSpan if possible.
pub fn resolve_breakpoints(
    args: &SetBreakpointsArguments,
    ast: &AstModule,
) -> anyhow::Result<ResolvedBreakpoints> {
    implementation::resolve_breakpoints(args, ast)
}

/// This is sort of the evaluation side of the DapAdapter. It's expected that these are on different threads
/// (the starlark evaluation is single-threaded, so certainly the DapAdapter itself doesn't do interesting
/// things there).
pub trait DapAdapterEvalHook: Debug + Send + 'static {
    /// Hooks the evaluator for this DapAdapter.
    fn add_dap_hooks<'v, 'a>(self: Box<Self>, eval: &mut Evaluator<'v, 'a>);
}

/// The DAP capabilities that the adapter supports.
pub fn dap_capabilities() -> Capabilities {
    Capabilities {
        supports_configuration_done_request: Some(true),
        supports_evaluate_for_hovers: Some(true),
        supports_set_variable: Some(true),
        supports_step_in_targets_request: Some(true),
        supports_conditional_breakpoints: Some(true),
        ..Capabilities::default()
    }
}

/// Creates a DapAdapter and corresponding DapAdapterEvalHook.
pub fn prepare_dap_adapter(
    client: Box<dyn DapAdapterClient>,
) -> (impl DapAdapter, impl DapAdapterEvalHook) {
    implementation::prepare_dap_adapter(client)
}


/*
 * Copyright 2019 The Starlark in Rust Authors.
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#[cfg(test)]
mod t {
    use std::collections::HashMap;
    use std::sync::atomic::AtomicUsize;
    use std::sync::atomic::Ordering;
    use std::sync::Arc;
    use std::thread::ScopedJoinHandle;
    use std::time::Duration;
    use std::time::Instant;

    use debugserver_types::*;
    use dupe::Dupe;

    use crate::assert::test_functions;
    use crate::debug::adapter::implementation::prepare_dap_adapter;
    use crate::debug::adapter::implementation::resolve_breakpoints;
    use crate::debug::DapAdapter;
    use crate::debug::DapAdapterClient;
    use crate::debug::DapAdapterEvalHook;
    use crate::debug::StepKind;
    use crate::environment::GlobalsBuilder;
    use crate::environment::Module;
    use crate::eval::Evaluator;
    use crate::eval::ReturnFileLoader;
    use crate::syntax::AstModule;
    use crate::syntax::Dialect;
    use crate::values::OwnedFrozenValue;
    use crate::wasm::is_wasm;

    #[derive(Debug)]
    struct Client {
        breakpoints_hit: Arc<AtomicUsize>,
    }

    impl Client {
        pub fn new(breakpoints_hit: Arc<AtomicUsize>) -> Self {
            Self { breakpoints_hit }
        }
    }

    impl DapAdapterClient for Client {
        fn event_stopped(&self) {
            println!("stopped!");
            self.breakpoints_hit.fetch_add(1, Ordering::SeqCst);
        }
    }

    struct BreakpointController {
        breakpoints_hit: Arc<AtomicUsize>,
    }

    impl BreakpointController {
        fn new() -> Self {
            Self {
                breakpoints_hit: Arc::new(AtomicUsize::new(0)),
            }
        }

        fn get_client(&self) -> Box<dyn DapAdapterClient> {
            Box::new(Client::new(self.breakpoints_hit.dupe()))
        }

        fn wait_for_eval_stopped(&self, breakpoint_count: usize, timeout: Duration) {
            let now = Instant::now();
            while self.breakpoints_hit.load(Ordering::SeqCst) != breakpoint_count {
                if now.elapsed() > timeout {
                    panic!("didn't hit expected breakpoint");
                }
                std::hint::spin_loop();
            }
        }
    }

    fn breakpoint(line: i64, condition: Option<&str>) -> SourceBreakpoint {
        SourceBreakpoint {
            column: None,
            condition: condition.map(|v| v.to_owned()),
            hit_condition: None,
            line,
            log_message: None,
        }
    }

    fn breakpoints_args(path: &str, lines: &[(i64, Option<&str>)]) -> SetBreakpointsArguments {
        SetBreakpointsArguments {
            breakpoints: Some(
                lines
                    .iter()
                    .map(|(line, condition)| breakpoint(*line, condition.as_deref()))
                    .collect(),
            ),
            lines: None,
            source: Source {
                adapter_data: None,
                checksums: None,
                name: None,
                origin: None,
                path: Some(path.to_owned()),
                presentation_hint: None,
                source_reference: None,
                sources: None,
            },
            source_modified: None,
        }
    }

    fn eval_with_hook(
        ast: AstModule,
        hook: impl DapAdapterEvalHook,
    ) -> anyhow::Result<OwnedFrozenValue> {
        let modules = HashMap::new();
        let loader = ReturnFileLoader { modules: &modules };
        let globals = GlobalsBuilder::extended().with(test_functions).build();
        let env = Module::new();
        let res = {
            let mut eval = Evaluator::new(&env);
            Box::new(hook).add_dap_hooks(&mut eval);
            eval.set_loader(&loader);
            eval.eval_module(ast, &globals)?
        };

        env.set("_", res);
        Ok(env
            .freeze()
            .expect("error freezing module")
            .get("_")
            .unwrap())
    }

    fn join_timeout<T>(waiting: ScopedJoinHandle<T>, timeout: Duration) -> T {
        let start = Instant::now();
        while !waiting.is_finished() {
            if start.elapsed() > timeout {
                panic!();
            }
        }
        waiting.join().unwrap()
    }

    static TIMEOUT: Duration = Duration::from_secs(10);

    #[test]
    fn test_breakpoint() -> anyhow::Result<()> {
        if is_wasm() {
            // `thread::scope` doesn't work in wasm.
            return Ok(());
        }

        let controller = BreakpointController::new();
        let (adapter, eval_hook) = prepare_dap_adapter(controller.get_client());
        let file_contents = "
x = [1, 2, 3]
print(x)
        ";
        std::thread::scope(|s| {
            let ast = AstModule::parse("test.bzl", file_contents.to_owned(), &Dialect::Extended)?;
            let breakpoints =
                resolve_breakpoints(&breakpoints_args("test.bzl", &[(3, None)]), &ast)?;
            adapter.set_breakpoints("test.bzl", &breakpoints)?;
            let eval_result =
                s.spawn(move || -> anyhow::Result<_> { eval_with_hook(ast, eval_hook) });
            controller.wait_for_eval_stopped(1, TIMEOUT);
            // TODO(cjhopman): we currently hit breakpoints on top-level statements twice (once for the gc bytecode, once for the actual statement).
            adapter.continue_()?;
            controller.wait_for_eval_stopped(2, TIMEOUT);

            adapter.continue_()?;

            join_timeout(eval_result, TIMEOUT)?;
            Ok(())
        })
    }

    #[test]
    fn test_breakpoint_with_failing_condition() -> anyhow::Result<()> {
        if is_wasm() {
            return Ok(());
        }

        let controller = BreakpointController::new();
        let (adapter, eval_hook) = prepare_dap_adapter(controller.get_client());
        let file_contents = "
x = [1, 2, 3]
print(x)
        ";
        std::thread::scope(|s| {
            let ast = AstModule::parse("test.bzl", file_contents.to_owned(), &Dialect::Extended)?;
            let breakpoints =
                resolve_breakpoints(&breakpoints_args("test.bzl", &[(3, Some("5 in x"))]), &ast)?;
            adapter.set_breakpoints("test.bzl", &breakpoints)?;
            let eval_result =
                s.spawn(move || -> anyhow::Result<_> { eval_with_hook(ast, eval_hook) });
            join_timeout(eval_result, TIMEOUT)?;
            Ok(())
        })
    }

    #[test]
    fn test_breakpoint_with_passing_condition() -> anyhow::Result<()> {
        if is_wasm() {
            return Ok(());
        }

        let controller = BreakpointController::new();
        let (adapter, eval_hook) = prepare_dap_adapter(controller.get_client());
        let file_contents = "
x = [1, 2, 3]
print(x)
        ";
        std::thread::scope(|s| {
            let ast = AstModule::parse("test.bzl", file_contents.to_owned(), &Dialect::Extended)?;
            let breakpoints =
                resolve_breakpoints(&breakpoints_args("test.bzl", &[(3, Some("2 in x"))]), &ast)?;
            adapter.set_breakpoints("test.bzl", &breakpoints)?;
            let eval_result =
                s.spawn(move || -> anyhow::Result<_> { eval_with_hook(ast, eval_hook) });
            controller.wait_for_eval_stopped(1, TIMEOUT);
            adapter.continue_()?;
            // TODO(cjhopman): we currently hit breakpoints on top-level statements twice (once for the gc bytecode, once for the actual statement).
            controller.wait_for_eval_stopped(2, TIMEOUT);
            adapter.continue_()?;

            join_timeout(eval_result, TIMEOUT)?;
            Ok(())
        })
    }

    #[test]
    fn test_step_over() -> anyhow::Result<()> {
        if is_wasm() {
            return Ok(());
        }

        let controller = BreakpointController::new();
        let (adapter, eval_hook) = prepare_dap_adapter(controller.get_client());
        let file_contents = "
def adjust(y):
    y[0] += 1
    y[1] += 1 # line 4
    y[2] += 1
x = [1, 2, 3]
adjust(x) # line 7
adjust(x)
print(x)
        ";
        std::thread::scope(|s| {
            let ast = AstModule::parse("test.bzl", file_contents.to_owned(), &Dialect::Extended)?;
            let breakpoints =
                resolve_breakpoints(&breakpoints_args("test.bzl", &[(7, None)]), &ast)?;
            adapter.set_breakpoints("test.bzl", &breakpoints)?;
            let eval_result =
                s.spawn(move || -> anyhow::Result<_> { eval_with_hook(ast, eval_hook) });
            controller.wait_for_eval_stopped(1, TIMEOUT);
            // TODO(cjhopman): we currently hit breakpoints on top-level statements twice (once for the gc bytecode, once for the actual statement).
            adapter.continue_()?;
            controller.wait_for_eval_stopped(2, TIMEOUT);

            assert_eq!("[1, 2, 3]", adapter.evaluate("x")?.result);
            adapter.step(StepKind::Over)?;
            controller.wait_for_eval_stopped(3, TIMEOUT);
            assert_eq!("[2, 3, 4]", adapter.evaluate("x")?.result);

            // TODO(cjhopman): we currently hit breakpoints on top-level statements twice (once for the gc bytecode, once for the actual statement).
            adapter.step(StepKind::Over)?;
            controller.wait_for_eval_stopped(4, TIMEOUT);
            adapter.step(StepKind::Over)?;
            controller.wait_for_eval_stopped(5, TIMEOUT);
            assert_eq!("[3, 4, 5]", adapter.evaluate("x")?.result);
            adapter.continue_()?;
            join_timeout(eval_result, TIMEOUT)?;
            Ok(())
        })
    }

    #[test]
    fn test_step_into() -> anyhow::Result<()> {
        if is_wasm() {
            return Ok(());
        }

        let controller = BreakpointController::new();
        let (adapter, eval_hook) = prepare_dap_adapter(controller.get_client());
        let file_contents = "
def adjust(y):
    y[0] += 1
    y[1] += 1 # line 4
    y[2] += 1
x = [1, 2, 3]
adjust(x) # line 7
adjust(x)
print(x)
        ";
        std::thread::scope(|s| {
            let ast = AstModule::parse("test.bzl", file_contents.to_owned(), &Dialect::Extended)?;
            let breakpoints =
                resolve_breakpoints(&breakpoints_args("test.bzl", &[(7, None)]), &ast)?;
            adapter.set_breakpoints("test.bzl", &breakpoints)?;
            let eval_result =
                s.spawn(move || -> anyhow::Result<_> { eval_with_hook(ast, eval_hook) });
            controller.wait_for_eval_stopped(1, TIMEOUT);
            // TODO(cjhopman): we currently hit breakpoints on top-level statements twice (once for the gc bytecode, once for the actual statement).
            adapter.continue_()?;
            controller.wait_for_eval_stopped(2, TIMEOUT);

            assert_eq!("[1, 2, 3]", adapter.evaluate("x")?.result);

            // into adjust
            adapter.step(StepKind::Into)?;
            controller.wait_for_eval_stopped(3, TIMEOUT);
            assert_eq!("[1, 2, 3]", adapter.evaluate("y")?.result);

            // into should go to next line
            adapter.step(StepKind::Into)?;
            controller.wait_for_eval_stopped(4, TIMEOUT);
            assert_eq!("[2, 2, 3]", adapter.evaluate("y")?.result);
            // two more intos should get us out of the function call
            adapter.step(StepKind::Into)?;
            controller.wait_for_eval_stopped(5, TIMEOUT);
            adapter.step(StepKind::Into)?;
            controller.wait_for_eval_stopped(6, TIMEOUT);
            assert_eq!("[2, 3, 4]", adapter.evaluate("x")?.result);

            // and once more back into the function
            adapter.step(StepKind::Into)?;
            controller.wait_for_eval_stopped(7, TIMEOUT);

            // TODO(cjhopman): unfortunately, gc being marked as statements causes us to need to step_into again.
            adapter.step(StepKind::Into)?;
            controller.wait_for_eval_stopped(8, TIMEOUT);

            assert_eq!("[2, 3, 4]", adapter.evaluate("y")?.result);

            adapter.continue_()?;
            join_timeout(eval_result, TIMEOUT)?;
            Ok(())
        })
    }

    #[test]
    fn test_step_out() -> anyhow::Result<()> {
        if is_wasm() {
            return Ok(());
        }

        let controller = BreakpointController::new();
        let (adapter, eval_hook) = prepare_dap_adapter(controller.get_client());
        let file_contents = "
def adjust(y):
    y[0] += 1
    y[1] += 1 # line 4
    y[2] += 1
x = [1, 2, 3]
adjust(x) # line 7
adjust(x)
print(x)
        ";
        std::thread::scope(|s| {
            let ast = AstModule::parse("test.bzl", file_contents.to_owned(), &Dialect::Extended)?;
            let breakpoints =
                resolve_breakpoints(&breakpoints_args("test.bzl", &[(4, None)]), &ast)?;
            adapter.set_breakpoints("test.bzl", &breakpoints)?;
            let eval_result =
                s.spawn(move || -> anyhow::Result<_> { eval_with_hook(ast, eval_hook) });
            // should break on the first time hitting line 4
            controller.wait_for_eval_stopped(1, TIMEOUT);
            assert_eq!("[2, 2, 3]", adapter.evaluate("y")?.result);

            // step out should take us to line 8
            adapter.step(StepKind::Out)?;
            controller.wait_for_eval_stopped(2, TIMEOUT);
            assert_eq!("[2, 3, 4]", adapter.evaluate("x")?.result);

            // step out should actually hit the breakpoint at 4 first (before getting out)
            adapter.step(StepKind::Out)?;
            controller.wait_for_eval_stopped(3, TIMEOUT);
            assert_eq!("[3, 3, 4]", adapter.evaluate("y")?.result);

            // step out should get out to the print
            adapter.step(StepKind::Out)?;
            controller.wait_for_eval_stopped(4, TIMEOUT);
            assert_eq!("[3, 4, 5]", adapter.evaluate("x")?.result);

            // one more out should be equivalent to continue
            adapter.step(StepKind::Out)?;
            join_timeout(eval_result, TIMEOUT)?;
            Ok(())
        })
    }
}


/*
 * Copyright 2019 The Starlark in Rust Authors.
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

use dupe::Dupe;
use itertools::Itertools;
use starlark_map::small_map::SmallMap;

use crate::docs::Doc;
use crate::docs::DocFunction;
use crate::docs::DocItem;
use crate::docs::DocMember;
use crate::docs::DocModule;
use crate::docs::DocObject;
use crate::docs::DocParam;
use crate::docs::DocProperty;
use crate::docs::DocString;
use crate::docs::DocType;

/// The style of output that is being generated
#[derive(Copy, Clone, Dupe)]
pub enum MarkdownFlavor {
    /// A file that is written out to disk for a website or in repo.
    ///
    /// These pages are generally slightly more detailed (e.g. module summary tables at the top
    /// of the page) and have different formatting due differing use cases.
    DocFile,
    /// A summary that can be shown in the "Hover" event in the LSP.
    LspSummary,
}

/// This object can potentially generate markdown documentation about itself.
pub trait RenderMarkdown {
    /// Generate markdown of the given flavor if possible. For some types, there may not be
    /// any useful documentation available.
    fn render_markdown_opt(&self, flavor: MarkdownFlavor) -> Option<String>;

    /// Convenience method that invokes `RenderMarkdown::render_markdown_opt`, and returns an
    /// empty string if that is `None`
    fn render_markdown(&self, flavor: MarkdownFlavor) -> String {
        self.render_markdown_opt(flavor).unwrap_or_default()
    }
}

impl RenderMarkdown for String {
    fn render_markdown_opt(&self, _flavor: MarkdownFlavor) -> Option<String> {
        Some(self.clone())
    }
}

impl RenderMarkdown for str {
    fn render_markdown_opt(&self, _flavor: MarkdownFlavor) -> Option<String> {
        Some(self.to_owned())
    }
}

/// What to render from a [`DocString`].
enum DSOpts {
    /// Just the summary.
    Summary,
    /// Just the details (if present).
    Details,
    /// Both the summary and the details, separated in an appropriate fashion.
    Combined,
}

fn render_doc_string(opts: DSOpts, string: &Option<DocString>) -> Option<String> {
    string.as_ref().and_then(|d| match opts {
        DSOpts::Summary => Some(d.summary.clone()),
        DSOpts::Details => d.details.clone(),
        DSOpts::Combined => Some(match &d.details {
            Some(details) => format!("{}\n\n{}", d.summary, details),
            None => d.summary.clone(),
        }),
    })
}

/// Function names can have underscores in them, which are markdown,
/// so escape them if we render them outside a codeblock.
fn escape_name(name: &str) -> String {
    name.replace('_', "\\_")
}

fn render_property(name: &str, property: &DocProperty) -> String {
    let prototype = render_code_block(&format!(
        "{name}: {}",
        TypeRenderer::Type(&property.typ).render_markdown(MarkdownFlavor::DocFile)
    ));
    let header = format!("## {}\n\n{prototype}", escape_name(name));
    let summary = render_doc_string(DSOpts::Summary, &property.docs);
    let details = render_doc_string(DSOpts::Details, &property.docs);

    let mut body = header;
    if let Some(summary) = summary {
        body.push_str("\n\n");
        body.push_str(&summary);
    }
    if let Some(details) = details {
        body.push_str("\n\n");
        body.push_str(&details);
    }

    body
}

/// If there are any parameter docs to render, render them as a list.
fn render_function_parameters(params: &[DocParam]) -> Option<String> {
    // Filter out parameters without docs
    let has_docs: Vec<_> = params
        .iter()
        .filter(|p| match p {
            DocParam::Arg { docs, .. } => docs.is_some(),
            DocParam::NoArgs | DocParam::OnlyPosBefore => false,
            DocParam::Args { docs, .. } => docs.is_some(),
            DocParam::Kwargs { docs, .. } => docs.is_some(),
        })
        .collect();

    if has_docs.is_empty() {
        return None;
    }

    let param_list: String = has_docs
        .iter()
        .filter_map(|p| match p {
            DocParam::Arg { name, docs, .. } => Some((name, docs)),
            DocParam::NoArgs | DocParam::OnlyPosBefore => None,
            DocParam::Args { name, docs, .. } => Some((name, docs)),
            DocParam::Kwargs { name, docs, .. } => Some((name, docs)),
        })
        .map(|(name, docs)| {
            let docs = render_doc_string(DSOpts::Combined, docs).unwrap_or_default();
            format!("* `{name}`: {docs}\n")
        })
        .collect();
    Some(param_list)
}

fn render_function(name: &str, function: &DocFunction) -> String {
    let prototype = render_code_block(
        &(TypeRenderer::Function {
            function_name: name,
            f: function,
        }
        .render_markdown(MarkdownFlavor::DocFile)),
    );
    let header = format!("## {}\n\n{prototype}", escape_name(name));
    let summary = render_doc_string(DSOpts::Summary, &function.docs);
    let details = render_doc_string(DSOpts::Details, &function.docs);

    let parameter_docs = render_function_parameters(&function.params);
    let return_docs = render_doc_string(DSOpts::Combined, &function.ret.docs);

    let mut body = header;
    if let Some(summary) = &summary {
        body.push_str("\n\n");
        body.push_str(summary);
    }
    if let Some(parameter_docs) = &parameter_docs {
        body.push_str("\n\n#### Parameters\n\n");
        body.push_str(parameter_docs);
    }
    if let Some(returns) = &return_docs {
        body.push_str("\n\n#### Returns\n\n");
        body.push_str(returns);
    }
    if let Some(dot_type) = &function.dot_type {
        body.push_str("\n\n#### `.type` attribute\n\n");
        body.push_str(&format!("Produces `{dot_type:?}`"));
    }
    if let Some(details) = &details {
        if parameter_docs.is_some() || return_docs.is_some() || function.dot_type.is_some() {
            body.push_str("\n\n#### Details\n\n");
        } else {
            // No need to aggressively separate the defaults from the summary if there
            // was nothing in between them. Just let it flow.
            body.push_str("\n\n");
        }
        body.push_str(details);
    }

    body
}

fn render_members(
    name: &str,
    object: bool,
    docs: &Option<DocString>,
    members: &SmallMap<String, DocMember>,
) -> String {
    // If this is a native, top level object, render it with a larger
    // header. Sub objects will be listed along side members, so use
    // smaller headers there.
    let title = if object {
        format!("# `{name}` type")
    } else {
        format!("# {name}")
    };
    let summary = render_doc_string(DSOpts::Combined, docs)
        .map(|s| format!("\n\n{}", s))
        .unwrap_or_default();

    let prefix = if object {
        format!("{name}.")
    } else {
        String::new()
    };

    let member_details: Vec<String> = members
        .iter()
        .sorted_by(|(l_m, _), (r_m, _)| l_m.cmp(r_m))
        .map(|(child, member)| render_member(&format!("{prefix}{child}"), member))
        .collect();
    let members_details = member_details.join("\n\n---\n\n");

    format!("{title}{summary}\n\n{members_details}")
}

/// Render a top level module.
fn render_module(name: &str, module: &DocModule) -> String {
    render_members(name, false, &module.docs, &module.members)
}

fn render_object(name: &str, object: &DocObject) -> String {
    render_members(name, true, &object.docs, &object.members)
}

fn render_doc_item(name: &str, item: &DocItem) -> String {
    match &item {
        DocItem::Module(m) => render_module(name, m),
        DocItem::Object(o) => render_object(name, o),
        DocItem::Function(f) => render_function(name, f),
        DocItem::Property(p) => render_property(name, p),
    }
}

impl RenderMarkdown for Doc {
    fn render_markdown_opt(&self, flavor: MarkdownFlavor) -> Option<String> {
        match flavor {
            MarkdownFlavor::DocFile => Some(render_doc_item(&self.id.name, &self.item)),
            MarkdownFlavor::LspSummary => None,
        }
    }
}

fn render_member(name: &str, member: &DocMember) -> String {
    match member {
        DocMember::Property(p) => render_property(name, p),
        DocMember::Function(f) => render_function(name, f),
    }
}

/// Any functions with more parameters than this will have
/// their prototype split over multiple lines. Otherwise, it is returned as
/// a single line.
const MAX_ARGS_BEFORE_MULTILINE: usize = 3;

/// If the prototype ends up longer than this length, we'll split it anyway
const MAX_LENGTH_BEFORE_MULTILINE: usize = 80;

/// Render a "type". This is either a [`Type`] object, or details about a function to
/// produce a function prototype.
enum TypeRenderer<'a> {
    /// A general "type".
    Type(&'a Option<DocType>),
    /// A function, with some extra formatting options.
    Function {
        /// The function name in the prototype as well.
        function_name: &'a str,
        f: &'a DocFunction,
    },
}

impl<'a> RenderMarkdown for TypeRenderer<'a> {
    fn render_markdown_opt(&self, flavor: MarkdownFlavor) -> Option<String> {
        fn raw_type(t: &Option<DocType>) -> String {
            match t {
                Some(t) => t.raw_type.to_string(),
                _ => "\"\"".to_owned(),
            }
        }

        fn raw_type_prefix(prefix: &str, t: &Option<DocType>) -> String {
            if t.is_some() {
                format!("{prefix}{}", raw_type(t))
            } else {
                String::new()
            }
        }

        match flavor {
            MarkdownFlavor::DocFile => match self {
                TypeRenderer::Type(t) => Some(raw_type(t)),
                TypeRenderer::Function { function_name, f } => {
                    let mut params = f.params.iter().map(|p| match p {
                        DocParam::Arg {
                            typ,
                            name,
                            default_value,
                            ..
                        } => {
                            let type_string = raw_type_prefix(": ", typ);
                            match default_value {
                                Some(v) => format!("{}{} = {}", name, type_string, v),
                                None => format!("{}{}", name, type_string),
                            }
                        }
                        DocParam::NoArgs => "*".to_owned(),
                        DocParam::OnlyPosBefore => "/".to_owned(),
                        DocParam::Args { typ, name, .. } => {
                            format!("{}{}", name, raw_type_prefix(": ", typ))
                        }
                        DocParam::Kwargs { typ, name, .. } => {
                            format!("{}{}", name, raw_type_prefix(": ", typ))
                        }
                    });

                    let ret_type = raw_type_prefix(" -> ", &f.ret.typ);
                    let prefix = format!("def {}", function_name);
                    let single_line_result =
                        format!("{}({}){}", prefix, params.clone().join(", "), ret_type);

                    if f.params.len() > MAX_ARGS_BEFORE_MULTILINE
                        || single_line_result.len() > MAX_LENGTH_BEFORE_MULTILINE
                    {
                        let chunked_params = params.join(",\n    ");
                        Some(format!(
                            "{}(\n    {}\n){}",
                            prefix, chunked_params, ret_type
                        ))
                    } else {
                        Some(single_line_result)
                    }
                }
            },
            MarkdownFlavor::LspSummary => None,
        }
    }
}

fn render_code_block(contents: &str) -> String {
    format!("```python\n{contents}\n```")
}