text:
//! Core processing module
//!
//! It causes me great pain that I can't figure out how split these methods up. The fact that we are relying on
//! lifetime coersion to reuse the `shuffler` vector really locks down the possible options.
//!
//! If we go with a dyn trait on the line splitter function it is appreciably slower.
use crate::{
    field_range::{FieldRange, RegexOrString},
    line_parser::LineParser,
    mmap::MmapChoice,
    single_byte_delim_parser::SingleByteDelimParser,
};
use anyhow::Result;
use bstr::ByteSlice;
use flate2::read::MultiGzDecoder;
use grep_cli::DecompressionReaderBuilder;
use regex::bytes::Regex;
use ripline::{
    line_buffer::{LineBuffer, LineBufferReader},
    lines::{self, LineIter},
    LineTerminator,
};
use std::{
    fs::File,
    io::{self, BufRead, BufReader, Read, Write},
    path::Path,
};

const DEFAULT_DELIM: &[u8] = &[b'\t'];

/// The input types that `hck` can parse.
pub enum HckInput<P: AsRef<Path>> {
    Stdin,
    Path(P),
}

/// The config object for [`Core`].
#[derive(Debug, Clone)]
pub struct CoreConfig<'a> {
    delimiter: &'a [u8],
    output_delimiter: &'a [u8],
    line_terminator: LineTerminator,
    mmap_choice: MmapChoice,
    is_parser_regex: bool,
    try_decompress: bool,
    raw_fields: Option<&'a str>,
    raw_header_fields: Option<&'a [Regex]>,
    raw_exclude: Option<&'a str>,
    raw_exclude_headers: Option<&'a [Regex]>,
    header_is_regex: bool,
    parsed_delim: RegexOrString,
}

impl<'a> Default for CoreConfig<'a> {
    fn default() -> Self {
        Self {
            delimiter: DEFAULT_DELIM,
            output_delimiter: DEFAULT_DELIM,
            line_terminator: LineTerminator::default(),
            mmap_choice: unsafe { MmapChoice::auto() },
            is_parser_regex: false,
            try_decompress: false,
            raw_fields: Some("1-"),
            raw_header_fields: None,
            raw_exclude: None,
            raw_exclude_headers: None,
            header_is_regex: false,
            parsed_delim: RegexOrString::String(
                std::str::from_utf8(DEFAULT_DELIM).unwrap().to_string(),
            ),
        }
    }
}

impl<'a> CoreConfig<'a> {
    /// Get the parsed delimiter
    pub fn parsed_delim(&self) -> &RegexOrString {
        &self.parsed_delim
    }

    /// Read the first line of an input and return it.
    ///
    /// It's up to the user to make sure that any consumed bytes are properly handed
    /// off to the line parsers later on.
    pub fn peek_first_line<P: AsRef<Path>>(
        &self,
        input: &HckInput<P>,
    ) -> Result<Vec<u8>, io::Error> {
        let mut buffer = String::new();
        match input {
            HckInput::Stdin => {
                // TODO: work out how to decode just a byte slice
                if self.try_decompress {
                    unimplemented!("Header selections not supported when piping gzipped stdin")
                }
                io::stdin().read_line(&mut buffer)?;
            }

            HckInput::Path(path) => {
                if self.try_decompress {
                    let reader: Box<dyn Read> = if path
                        .as_ref()
                        .to_str()
                        .map(|p| p.ends_with(".gz"))
                        .unwrap_or(false)
                    {
                        Box::new(MultiGzDecoder::new(File::open(path)?))
                    } else {
                        Box::new(
                            DecompressionReaderBuilder::new()
                                // .matcher(matcher)
                                .build(path)?,
                        )
                    };
                    let mut reader = BufReader::new(reader);
                    reader.read_line(&mut buffer)?;
                } else {
                    BufReader::new(File::open(path)?).read_line(&mut buffer)?;
                }
            }
        }
        Ok(lines::without_terminator(buffer.as_bytes(), self.line_terminator).to_owned())
    }

    /// Parse the raw user input fields and header fields. Returns any header bytes read and the parsed fields
    pub fn parse_fields<P>(&self, input: &HckInput<P>) -> Result<(Option<Vec<u8>>, Vec<FieldRange>)>
    where
        P: AsRef<Path>,
    {
        // Parser the fields in the context of the files being looked at
        let (mut extra, fields) = match (self.raw_fields, self.raw_header_fields) {
            (Some(field_list), Some(header_fields)) => {
                let first_line = self.peek_first_line(input)?;
                let mut fields = FieldRange::from_list(field_list)?;
                let header_fields = FieldRange::from_header_list(
                    header_fields,
                    first_line.as_bytes(),
                    &self.parsed_delim,
                    self.header_is_regex,
                    false,
                )?;
                fields.extend(header_fields.into_iter());
                FieldRange::post_process_ranges(&mut fields);
                (Some(first_line), fields)
            }
            (Some(field_list), None) => (None, FieldRange::from_list(field_list)?),
            (None, Some(header_fields)) => {
                let first_line = self.peek_first_line(input)?;
                let fields = FieldRange::from_header_list(
                    header_fields,
                    first_line.as_bytes(),
                    &self.parsed_delim,
                    self.header_is_regex,
                    false,
                )?;
                (Some(first_line), fields)
            }
            (None, None) => (None, FieldRange::from_list("1-")?),
        };

        let fields = match (&self.raw_exclude, &self.raw_exclude_headers) {
            (Some(exclude), Some(exclude_header)) => {
                let exclude = FieldRange::from_list(exclude)?;
                let fields = FieldRange::exclude(fields, exclude);
                let first_line = if let Some(first_line) = extra {
                    first_line
                } else {
                    self.peek_first_line(input)?
                };
                let exclude_headers = FieldRange::from_header_list(
                    exclude_header,
                    first_line.as_bytes(),
                    &self.parsed_delim,
                    self.header_is_regex,
                    true,
                )?;
                extra = Some(first_line);
                FieldRange::exclude(fields, exclude_headers)
            }
            (Some(exclude), None) => {
                let exclude = FieldRange::from_list(exclude)?;
                FieldRange::exclude(fields, exclude)
            }
            (None, Some(exclude_header)) => {
                let first_line = if let Some(first_line) = extra {
                    first_line
                } else {
                    self.peek_first_line(input)?
                };
                let exclude_headers = FieldRange::from_header_list(
                    exclude_header,
                    first_line.as_bytes(),
                    &self.parsed_delim,
                    self.header_is_regex,
                    true,
                )?;
                extra = Some(first_line);
                FieldRange::exclude(fields, exclude_headers)
            }
            (None, None) => fields,
        };
        Ok((extra, fields))
    }
}

/// A builder for the [`CoreConfig`] which drives [`Core`].
#[derive(Clone, Debug)]
pub struct CoreConfigBuilder<'a> {
    config: CoreConfig<'a>,
}

impl<'a> CoreConfigBuilder<'a> {
    pub fn new() -> Self {
        Self {
            config: CoreConfig::default(),
        }
    }

    pub fn build(mut self) -> Result<CoreConfig<'a>> {
        let delim = if self.config.is_parser_regex {
            RegexOrString::Regex(Regex::new(self.config.delimiter.to_str()?)?)
        } else {
            let unescaped =
                std::str::from_utf8(&grep_cli::unescape(self.config.delimiter.to_str()?))?
                    .to_string();
            RegexOrString::String(unescaped)
        };
        self.config.parsed_delim = delim;
        Ok(self.config)
    }

    /// The substr to split lines on.
    pub fn delimiter(mut self, delim: &'a [u8]) -> Self {
        self.config.delimiter = delim;
        self
    }

    /// The substr to use as the output delimiter
    pub fn output_delimiter(mut self, delim: &'a [u8]) -> Self {
        self.config.output_delimiter = delim;
        self
    }

    /// The line terminator to use when looking for linebreaks and stripping linebreach chars.
    pub fn line_terminator(mut self, term: LineTerminator) -> Self {
        self.config.line_terminator = term;
        self
    }

    /// Whether or not to try to use mmap mode
    pub fn mmap(mut self, mmap_choice: MmapChoice) -> Self {
        self.config.mmap_choice = mmap_choice;
        self
    }

    /// Whether or not the parser is a regex
    #[allow(clippy::wrong_self_convention)]
    pub fn is_regex_parser(mut self, is_regex: bool) -> Self {
        self.config.is_parser_regex = is_regex;
        self
    }

    /// Try to decompress an input file
    pub fn try_decompress(mut self, try_decompress: bool) -> Self {
        self.config.try_decompress = try_decompress;
        self
    }

    /// The raw user input fields to output
    pub fn fields(mut self, fields: Option<&'a str>) -> Self {
        self.config.raw_fields = fields;
        self
    }

    /// The raw user input header to output
    pub fn headers(mut self, headers: Option<&'a [Regex]>) -> Self {
        self.config.raw_header_fields = headers;
        self
    }

    /// The raw user input fields to exclude
    pub fn exclude(mut self, exclude: Option<&'a str>) -> Self {
        self.config.raw_exclude = exclude;
        self
    }

    /// The raw user input headers to exclude
    pub fn exclude_headers(mut self, exclude_headers: Option<&'a [Regex]>) -> Self {
        self.config.raw_exclude_headers = exclude_headers;
        self
    }

    /// Whether or not to treat the headers as regex
    pub fn header_is_regex(mut self, header_is_regex: bool) -> Self {
        self.config.header_is_regex = header_is_regex;
        self
    }
}

impl<'a> Default for CoreConfigBuilder<'a> {
    fn default() -> Self {
        Self::new()
    }
}

/// The main processing loop
pub struct Core<'a, L> {
    /// The [`CoreConfig`] object that determines how [`Core`] is run
    config: &'a CoreConfig<'a>,
    /// The [`FieldRange`]'s to keep, in the order to output them
    fields: &'a [FieldRange],
    /// The reusable line parse that defines how to parse a line (regex or substr).
    line_parser: L,
    /// The reusable line buffer that holds bytes from reads
    line_buffer: &'a mut LineBuffer,
}

impl<'a, L> Core<'a, L>
where
    L: LineParser<'a>,
{
    /// Create a new "core" the can be used to parse multiple inputs
    pub fn new(
        config: &'a CoreConfig,
        fields: &'a [FieldRange],
        line_parser: L,
        line_buffer: &'a mut LineBuffer,
    ) -> Self {
        Self {
            config,
            fields,
            line_parser,
            line_buffer,
        }
    }

    /// Check if no reordering of fields is happening
    #[inline]
    fn are_fields_pos_sorted(&self) -> bool {
        let mut test = 0;
        for field in self.fields {
            if field.pos < test {
                return false;
            }
            test = field.pos
        }
        true
    }

    /// Check if we can run in `fast mode`.
    ///
    /// delimiter is 1 byte, newline is 1 bytes, and we are not using a regex
    fn allow_fastmode(&self) -> bool {
        self.config.delimiter.len() == 1
            && self.config.line_terminator.as_bytes().len() == 1
            && !self.config.is_parser_regex
            && self.are_fields_pos_sorted()
    }

    pub fn hck_input<P, W>(
        &mut self,
        input: HckInput<P>,
        mut output: W,
        header: Option<Vec<u8>>,
    ) -> Result<(), io::Error>
    where
        P: AsRef<Path>,
        W: Write,
    {
        // Dispatch to a given `hck_*` runner depending on configuration
        match input {
            HckInput::Stdin => {
                if let Some(header) = header {
                    self.hck_bytes(header.as_bytes(), &mut output)?;
                }
                let reader: Box<dyn Read> = if self.config.try_decompress {
                    Box::new(MultiGzDecoder::new(io::stdin()))
                } else {
                    Box::new(io::stdin())
                };
                if self.allow_fastmode() {
                    self.hck_reader_fast(reader, &mut output)
                } else {
                    self.hck_reader(reader, &mut output)
                }
            }
            HckInput::Path(path) => {
                if self.config.try_decompress {
                    let reader: Box<dyn Read> = if path
                        .as_ref()
                        .to_str()
                        .map(|p| p.ends_with(".gz"))
                        .unwrap_or(false)
                    {
                        Box::new(MultiGzDecoder::new(File::open(&path)?))
                    } else {
                        Box::new(
                            DecompressionReaderBuilder::new()
                                // .matcher(matcher)
                                .build(&path)?,
                        )
                    };
                    if self.allow_fastmode() {
                        self.hck_reader_fast(reader, &mut output)
                    } else {
                        self.hck_reader(reader, &mut output)
                    }
                } else {
                    let file = File::open(&path)?;
                    if let Some(mmap) = self.config.mmap_choice.open(&file, Some(&path)) {
                        if self.allow_fastmode() {
                            self.hck_bytes_fast(mmap.as_bytes(), &mut output)
                        } else {
                            self.hck_bytes(mmap.as_bytes(), &mut output)
                        }
                    } else if self.allow_fastmode() {
                        self.hck_reader_fast(file, &mut output)
                    } else {
                        self.hck_reader(file, &mut output)
                    }
                }
            }
        }
    }

    /// Iterate over the lines in a slice of bytes.
    ///
    /// The input slice of bytes is assumed to end in a newline.
    pub fn hck_bytes<W>(&mut self, bytes: &[u8], mut output: W) -> Result<(), io::Error>
    where
        W: Write,
    {
        let iter = LineIter::new(self.config.line_terminator.as_byte(), bytes.as_bytes());
        let mut shuffler: Vec<Vec<&'static [u8]>> =
            vec![vec![]; self.fields.iter().map(|f| f.pos).max().unwrap() + 1];
        for line in iter {
            let mut s: Vec<Vec<&[u8]>> = shuffler;
            self.line_parser.parse_line(
                lines::without_terminator(line, self.config.line_terminator),
                &mut s,
            );
            let items = s.iter_mut().flat_map(|s| s.drain(..));
            output.join_append(
                self.config.output_delimiter,
                items,
                &self.config.line_terminator,
            )?;
            shuffler = unsafe { core::mem::transmute(s) };
        }
        Ok(())
    }

    /// Fast mode iteration over lines in a slice of bytes.
    ///
    /// This expects the seperator to be a single byte and the newline to be a singel byte.
    ///
    /// Instead of  seaching for linebreaks, then splitting up the line on the `sep`,
    /// fast mode looks for either `sep` or `newline` at the same time, so instead of two passes
    /// over the bytes we only make one pass.
    pub fn hck_bytes_fast<W: Write>(&mut self, bytes: &[u8], output: W) -> Result<(), io::Error> {
        let mut buffer_parser = SingleByteDelimParser::new(
            self.config.line_terminator,
            self.config.output_delimiter,
            self.fields,
            self.config.delimiter[0],
        );
        buffer_parser.process_buffer(bytes, output)?;
        Ok(())
    }

    /// Fast mode iteration over lines in a reader.
    ///
    /// This expects the separator to be a single byte and the newline to be a single byte.
    ///
    /// Instead of  seaching for linebreaks, then splitting up the line on the `sep`,
    /// fast mode looks for either `sep` or `newline` at the same time, so instead of two passes
    /// over the bytes we only make one pass.
    pub fn hck_reader_fast<R: Read, W: Write>(
        &mut self,
        reader: R,
        mut output: W,
    ) -> Result<(), io::Error> {
        let mut reader = LineBufferReader::new(reader, self.line_buffer);
        let mut buffer_parser = SingleByteDelimParser::new(
            self.config.line_terminator,
            self.config.output_delimiter,
            self.fields,
            self.config.delimiter[0],
        );

        while reader.fill()? {
            buffer_parser.process_buffer(reader.buffer(), &mut output)?;
            buffer_parser.reset();
            reader.consume(reader.buffer().len());
        }
        Ok(())
    }

    /// Process lines from a reader.
    pub fn hck_reader<R: Read, W: Write>(
        &mut self,
        reader: R,
        mut output: W,
    ) -> Result<(), io::Error> {
        let mut reader = LineBufferReader::new(reader, self.line_buffer);
        let mut shuffler: Vec<Vec<&'static [u8]>> =
            vec![vec![]; self.fields.iter().map(|f| f.pos).max().unwrap() + 1];
        while reader.fill()? {
            let iter = LineIter::new(self.config.line_terminator.as_byte(), reader.buffer());

            for line in iter {
                let mut s: Vec<Vec<&[u8]>> = shuffler;
                self.line_parser.parse_line(
                    lines::without_terminator(line, self.config.line_terminator),
                    &mut s,
                );

                let items = s.iter_mut().flat_map(|s| s.drain(..));
                output.join_append(
                    self.config.output_delimiter,
                    items,
                    &self.config.line_terminator,
                )?;
                shuffler = unsafe { core::mem::transmute(s) };
            }
            reader.consume(reader.buffer().len());
        }
        Ok(())
    }
}

/// A trait for adding `join_append` to a writer.
pub trait JoinAppend {
    /// Given an input iterator of items, write them with a serparator and a newline.
    fn join_append<'b>(
        &mut self,
        sep: &[u8],
        items: impl Iterator<Item = &'b [u8]>,
        term: &LineTerminator,
    ) -> Result<(), io::Error>;
}

/// [`JoinAppend`] for [`Write`].
impl<W: Write> JoinAppend for W {
    /// Given an input iterator of items, write them with a serparator and a newline.
    #[inline(always)]
    fn join_append<'b>(
        &mut self,
        sep: &[u8],
        mut items: impl Iterator<Item = &'b [u8]>,
        term: &LineTerminator,
    ) -> Result<(), io::Error> {
        if let Some(item) = items.next() {
            self.write_all(item)?;
        }

        for item in items {
            self.write_all(sep)?;
            self.write_all(item)?;
        }
        self.write_all(term.as_bytes())?;
        Ok(())
    }
}


text:
#![allow(clippy::missing_errors_doc, clippy::missing_panics_doc)]
pub mod core;
pub mod field_range;
pub mod line_parser;
pub mod mmap;
pub mod single_byte_delim_parser;

repo_name: sstadick/hck

repo_language: Rust

mime_type: text/plain


text:
use std::fs::File;
use std::path::Path;

use memmap::Mmap;

/// Controls the strategy used for determining when to use memory maps.
///
/// If a searcher is called in circumstances where it is possible to use memory
/// maps, and memory maps are enabled, then it will attempt to do so if it
/// believes it will make the search faster.
///
/// By default, memory maps are disabled.
#[derive(Clone, Debug, Copy)]
pub struct MmapChoice(MmapChoiceImpl);

#[derive(Clone, Debug, Copy)]
enum MmapChoiceImpl {
    Auto,
    Never,
}

impl Default for MmapChoice {
    fn default() -> MmapChoice {
        MmapChoice(MmapChoiceImpl::Never)
    }
}

impl MmapChoice {
    /// Use memory maps when they are believed to be advantageous.
    ///
    /// The heuristics used to determine whether to use a memory map or not
    /// may depend on many things, including but not limited to, file size
    /// and platform.
    ///
    /// If memory maps are unavailable or cannot be used for a specific input,
    /// then normal OS read calls are used instead.
    ///
    /// # Safety
    ///
    /// This constructor is not safe because there is no obvious way to
    /// encapsulate the safety of file backed memory maps on all platforms
    /// without simultaneously negating some or all of their benefits.
    ///
    /// The specific contract the caller is required to uphold isn't precise,
    /// but it basically amounts to something like, "the caller guarantees that
    /// the underlying file won't be mutated." This, of course, isn't feasible
    /// in many environments. However, command line tools may still decide to
    /// take the risk of, say, a `SIGBUS` occurring while attempting to read a
    /// memory map.
    pub unsafe fn auto() -> MmapChoice {
        MmapChoice(MmapChoiceImpl::Auto)
    }

    /// Never use memory maps, no matter what. This is the default.
    pub fn never() -> MmapChoice {
        MmapChoice(MmapChoiceImpl::Never)
    }

    /// Return a memory map if memory maps are enabled and if creating a
    /// memory from the given file succeeded and if memory maps are believed
    /// to be advantageous for performance.
    ///
    /// If this does attempt to open a memory map and it fails, then `None`
    /// is returned and the corresponding error (along with the file path, if
    /// present) is logged at the debug level.
    pub(crate) fn open<P: AsRef<Path>>(&self, file: &File, path: Option<&P>) -> Option<Mmap> {
        if !self.is_enabled() {
            return None;
        }
        if !cfg!(target_pointer_width = "64") {
            // For 32-bit systems, it looks like mmap will succeed even if it
            // can't address the entire file. This seems to happen at least on
            // Windows, even though it uses to work prior to ripgrep 13. The
            // only Windows-related change in ripgrep 13, AFAIK, was statically
            // linking vcruntime. So maybe that's related? But I'm not sure.
            //
            // See: https://github.com/BurntSushi/ripgrep/issues/1911
            return None;
        }
        if cfg!(target_os = "macos") {
            // I guess memory maps on macOS aren't great. Should re-evaluate.
            return None;
        }
        // SAFETY: This is acceptable because the only way `MmapChoiceImpl` can
        // be `Auto` is if the caller invoked the `auto` constructor, which
        // is itself not safe. Thus, this is a propagation of the caller's
        // assertion that using memory maps is safe.
        match unsafe { Mmap::map(file) } {
            Ok(mmap) => Some(mmap),
            Err(err) => {
                if let Some(path) = path {
                    log::debug!(
                        "{}: failed to open memory map: {}",
                        path.as_ref().display(),
                        err
                    );
                } else {
                    log::debug!("failed to open memory map: {}", err);
                }
                None
            }
        }
    }

    /// Whether this strategy may employ memory maps or not.
    pub(crate) fn is_enabled(&self) -> bool {
        match self.0 {
            MmapChoiceImpl::Auto => true,
            MmapChoiceImpl::Never => false,
        }
    }
}


text:
//! Parse ranges like `-2,5,8-10,13-`.
//!
//! # Examples
//!
//! TODO

use bstr::ByteSlice;
use regex::bytes::Regex;
use std::{cmp::max, collections::VecDeque, str::FromStr};
use thiserror::Error;

/// The fartest right possible field
const MAX: usize = usize::MAX;

/// Errors for parsing / validating [`FieldRange`] strings.
#[derive(Error, Debug, PartialEq)]
pub enum FieldError {
    #[error("Header not found: {0}")]
    HeaderNotFound(String),
    #[error("Fields and positions are numbered from 1: {0}")]
    InvalidField(usize),
    #[error("High end of range less than low end of range: {0}-{1}")]
    InvalidOrder(usize, usize),
    #[error("Failed to parse field: {0}")]
    FailedParse(String),
    #[error("No headers matched")]
    NoHeadersMatched,
}

#[derive(Debug, Clone)]
pub enum RegexOrString {
    Regex(Regex),
    String(String),
}

impl RegexOrString {
    fn split<'a>(&'a self, line: &'a [u8]) -> Box<dyn Iterator<Item = &'a [u8]> + 'a> {
        match self {
            RegexOrString::Regex(r) => Box::new(r.split(line)),
            RegexOrString::String(s) => Box::new(line.split_str(s)),
        }
    }
}

/// Represent a range of columns to keep.
#[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Copy, Clone)]
pub struct FieldRange {
    pub low: usize,
    pub high: usize,
    // The initial position of this range in the user input
    pub pos: usize,
}

impl FromStr for FieldRange {
    type Err = FieldError;

    /// Convert a [`str`] into a [`FieldRange`]
    fn from_str(s: &str) -> Result<FieldRange, FieldError> {
        let mut parts = s.splitn(2, '-');

        match (parts.next(), parts.next()) {
            (Some(nm), None) => {
                if let Ok(nm) = nm.parse::<usize>() {
                    if nm > 0 {
                        Ok(FieldRange {
                            low: nm - 1,
                            high: nm - 1,
                            pos: 0,
                        })
                    } else {
                        Err(FieldError::InvalidField(nm))
                    }
                } else {
                    Err(FieldError::FailedParse(nm.to_owned()))
                }
            }
            (Some(n), Some(m)) if m.is_empty() => {
                if let Ok(low) = n.parse::<usize>() {
                    if low > 0 {
                        Ok(FieldRange {
                            low: low - 1,
                            high: MAX - 1,
                            pos: 0,
                        })
                    } else {
                        Err(FieldError::InvalidField(low))
                    }
                } else {
                    Err(FieldError::FailedParse(n.to_owned()))
                }
            }
            (Some(n), Some(m)) if n.is_empty() => {
                if let Ok(high) = m.parse::<usize>() {
                    if high > 0 {
                        Ok(FieldRange {
                            low: 0,
                            high: high - 1,
                            pos: 0,
                        })
                    } else {
                        Err(FieldError::InvalidField(high))
                    }
                } else {
                    Err(FieldError::FailedParse(m.to_owned()))
                }
            }
            (Some(n), Some(m)) => match (n.parse::<usize>(), m.parse::<usize>()) {
                (Ok(low), Ok(high)) => {
                    if low > 0 && low <= high {
                        Ok(FieldRange {
                            low: low - 1,
                            high: high - 1,
                            pos: 0,
                        })
                    } else if low == 0 {
                        Err(FieldError::InvalidField(low))
                    } else {
                        Err(FieldError::InvalidOrder(low, high))
                    }
                }
                _ => Err(FieldError::FailedParse(format!("{}-{}", n, m))),
            },
            _ => unreachable!(),
        }
    }
}

impl FieldRange {
    pub const fn default() -> Self {
        Self {
            low: 0,
            high: MAX - 1,
            pos: 0,
        }
    }

    /// Parse a comma separated list of fields and merge any overlaps
    pub fn from_list(list: &str) -> Result<Vec<FieldRange>, FieldError> {
        let mut ranges: Vec<FieldRange> = vec![];
        for (i, item) in list.split(',').enumerate() {
            let mut rnge: FieldRange = FromStr::from_str(item)?;
            rnge.pos = i;
            ranges.push(rnge);
        }
        FieldRange::post_process_ranges(&mut ranges);

        Ok(ranges)
    }

    /// Get the indices of the headers that match any of the provided regex's.
    pub fn from_header_list(
        list: &[Regex],
        header: &[u8],
        delim: &RegexOrString,
        header_is_regex: bool,
        allow_missing: bool,
    ) -> Result<Vec<FieldRange>, FieldError> {
        let mut ranges = vec![];
        let mut found = vec![false; list.len()];
        for (i, header) in delim.split(header).enumerate() {
            for (j, regex) in list.iter().enumerate() {
                if !header_is_regex {
                    if regex.as_str().as_bytes() == header {
                        found[j] = true;
                        ranges.push(FieldRange {
                            low: i,
                            high: i,
                            pos: j,
                        });
                    }
                } else if regex.is_match(header) {
                    found[j] = true;
                    ranges.push(FieldRange {
                        low: i,
                        high: i,
                        pos: j,
                    });
                }
            }
        }

        if !allow_missing {
            if ranges.is_empty() {
                return Err(FieldError::NoHeadersMatched);
            }
            for (i, was_found) in found.into_iter().enumerate() {
                if !was_found {
                    return Err(FieldError::HeaderNotFound(list[i].as_str().to_owned()));
                }
            }
        }

        FieldRange::post_process_ranges(&mut ranges);

        Ok(ranges)
    }

    // TODO: There is a very subtle bug in this sorting / mereging code.
    // The tests have been updated to reflect it. In addition to sometimes merging overlaps, sometimes not,
    // this will merge everlaps even when their positions are out of order, i.e. I want to output pos 2, then pos 1,
    // but this will merge that into a range covering 1-2 and forget the pos ordering.

    /// Sort and merge overlaps in a set of [`Vec<FieldRange>`].
    pub fn post_process_ranges(ranges: &mut Vec<FieldRange>) {
        ranges.sort();

        // merge overlapping ranges
        let mut shifted = 0;
        for i in 0..ranges.len() {
            let j = i + 1;
            if let Some(rng) = ranges.get_mut(i) {
                rng.pos = rng.pos.saturating_sub(shifted);
            }

            while j < ranges.len()
                && ranges[j].low <= ranges[i].high + 1
                && ranges[j].pos.saturating_sub(1) == ranges[i].pos
            {
                let j_high = ranges.remove(j).high;
                ranges[i].high = max(ranges[i].high, j_high);
                shifted += 1;
            }
        }
    }

    /// Test if a value is contained in this range
    pub fn contains(&self, value: usize) -> bool {
        value >= self.low && value <= self.high
    }

    /// Test if two ranges overlap
    pub fn overlap(&self, other: &Self) -> bool {
        self.low <= other.high && self.high >= other.low
    }

    /// Remove ranges in exclude from fields.
    ///
    /// This assumes both fields and exclude are in ascending order by `low` value.
    pub fn exclude(fields: Vec<FieldRange>, exclude: Vec<FieldRange>) -> Vec<FieldRange> {
        let mut fields: VecDeque<_> = fields.into_iter().collect();
        let mut result = vec![];
        let mut exclude_iter = exclude.into_iter();
        let mut exclusion = if let Some(ex) = exclude_iter.next() {
            ex
        } else {
            // Early return, no exclusions
            return fields.into_iter().collect();
        };
        let mut field = fields.pop_front().unwrap(); // Must have at least one field
        loop {
            // Determine if there is any overlap at all
            if exclusion.overlap(&field) {
                // Determine the type of overlap
                match (
                    exclusion.contains(field.low),
                    exclusion.contains(field.high),
                ) {
                    // Field: XXXXXXXX
                    // Exclu:      XXXXXXXX
                    (false, true) => {
                        if exclusion.low != 0 {
                            field.high = exclusion.low - 1;
                        }
                    }

                    // Field:    XXXXXXXX
                    // Exclu: XXXXX
                    (true, false) => {
                        if exclusion.high != MAX - 1 {
                            field.low = exclusion.high + 1;
                        }
                    }
                    // Field:    XXXXX
                    // Exclu: XXXXXXXXXX
                    (true, true) => {
                        // Skip since we are excluding all fields in this range
                        if let Some(f) = fields.pop_front() {
                            field = f;
                        } else {
                            break;
                        }
                    }

                    // Field: XXXXXXXXXX
                    // exclu:     XXXX
                    (false, false) => {
                        // Split the field
                        // high side
                        if exclusion.high != MAX - 1 {
                            let mut high_field = field;
                            high_field.low = exclusion.high + 1;
                            fields.push_front(high_field)
                        }

                        // low side
                        if exclusion.low != 0 {
                            field.high = exclusion.low - 1;
                        }
                    }
                }
            } else if field.low > exclusion.high {
                // if the exclusion is behind the field, advance the exclusion
                if let Some(ex) = exclude_iter.next() {
                    exclusion = ex;
                } else {
                    result.push(field);
                    result.extend(fields.into_iter());
                    break;
                }
            } else if field.high < exclusion.low {
                // if the exclusion is ahead of the field, push the field
                result.push(field);
                if let Some(f) = fields.pop_front() {
                    field = f;
                } else {
                    break;
                }
            } else {
                unreachable!()
            }
        }
        result
    }
}

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

    #[test]
    #[rustfmt::skip::macros(assert_eq)]
    fn test_parse_fields_good() {
        assert_eq!(vec![FieldRange { low: 0, high: 0, pos: 0}], FieldRange::from_list("1").unwrap());
        assert_eq!(vec![FieldRange { low: 0, high: 0, pos: 0},  FieldRange { low: 3, high: 3, pos: 1}], FieldRange::from_list("1,4").unwrap());
        assert_eq!(vec![FieldRange { low: 0, high: 1, pos: 0},  FieldRange { low: 3, high: usize::MAX - 1, pos: 1}], FieldRange::from_list("1,2,4-").unwrap());
        assert_eq!(vec![FieldRange { low: 1, high: 2, pos: 0},  FieldRange { low: 3, high: usize::MAX - 1, pos: 1} ], FieldRange::from_list("2,3,4-").unwrap());
        assert_eq!(vec![FieldRange { low: 0, high: 0, pos: 0},  FieldRange { low: 3, high: usize::MAX - 1, pos: 1}], FieldRange::from_list("1,4-,5-8").unwrap());
        assert_eq!(vec![FieldRange { low: 0, high: 0, pos: 1},  FieldRange { low: 3, high: usize::MAX - 1, pos: 0}, FieldRange { low: 4, high: 7, pos: 2}], FieldRange::from_list("4-,1,5-8").unwrap());
        assert_eq!(vec![FieldRange { low: 0, high: 3, pos: 0}], FieldRange::from_list("-4").unwrap());
        assert_eq!(vec![FieldRange { low: 0, high: 7, pos: 0}], FieldRange::from_list("-4,5-8").unwrap());
        assert_eq!(vec![FieldRange { low: 0, high: 0, pos: 1 }, FieldRange { low: 2, high: 2, pos: 0}, FieldRange { low: 2, high: 2, pos: 2}], FieldRange::from_list("3,1,3").unwrap());
        // Note the slightly odd pos ordering that happens here. This is an artifact of post_process_ranges, which needs some love
        assert_eq!(vec![FieldRange { low: 0, high: 1, pos: 0 }, FieldRange { low: 2, high: 2, pos: 1}, FieldRange { low: 3, high: 3, pos: 2}], FieldRange::from_list("1,2,3,4").unwrap());
        assert_eq!(vec![FieldRange { low: 0, high: 1, pos: 0 }, FieldRange { low: 2, high: 2, pos: 2}, FieldRange { low: 3, high: 3, pos: 1}], FieldRange::from_list("1,2,4,3").unwrap());
    }

    #[test]
    fn test_parse_fields_bad() {
        assert!(FieldRange::from_list("0").is_err());
        assert!(FieldRange::from_list("4-1").is_err());
        assert!(FieldRange::from_list("cat").is_err());
        assert!(FieldRange::from_list("1-dog").is_err());
        assert!(FieldRange::from_list("mouse-4").is_err());
    }

    #[test]
    fn test_parse_header_fields() {
        let header = b"is_cat-isdog-wascow-was_is_apple-12345-!$%*(_)";
        let delim = Regex::new("-").unwrap();
        let delim = RegexOrString::Regex(delim);
        let header_fields = vec![
            Regex::new(r"^is_.*$").unwrap(),
            Regex::new("dog").unwrap(),
            Regex::new(r"\$%").unwrap(),
        ];
        let fields =
            FieldRange::from_header_list(&header_fields, header, &delim, true, false).unwrap();
        assert_eq!(
            vec![
                FieldRange {
                    low: 0,
                    high: 1,
                    pos: 0
                },
                FieldRange {
                    low: 5,
                    high: 5,
                    pos: 1
                }
            ],
            fields
        );
    }

    #[test]
    fn test_parse_header_fields_literal() {
        let header = b"is_cat-is-isdog-wascow-was_is_apple-12345-!$%*(_)";
        let delim = Regex::new("-").unwrap();
        let delim = RegexOrString::Regex(delim);
        let header_fields = vec![Regex::new(r"is").unwrap()];
        let fields =
            FieldRange::from_header_list(&header_fields, header, &delim, false, false).unwrap();
        assert_eq!(
            vec![FieldRange {
                low: 1,
                high: 1,
                pos: 0
            },],
            fields
        );
    }

    #[test]
    fn test_parse_header_fields_literal_header_not_found() {
        let header = b"is_cat-is-isdog-wascow-was_is_apple-12345-!$%*(_)";
        let delim = Regex::new("-").unwrap();
        let delim = RegexOrString::Regex(delim);
        let header_fields = vec![
            Regex::new(r"^is_.*$").unwrap(),
            Regex::new("dog").unwrap(),
            Regex::new(r"\$%").unwrap(),
            Regex::new(r"is").unwrap(),
        ];
        let result = FieldRange::from_header_list(&header_fields, header, &delim, false, false);
        assert_eq!(
            result.unwrap_err(),
            FieldError::HeaderNotFound(String::from(r"^is_.*$"))
        );
    }

    #[test]
    #[rustfmt::skip::macros(assert_eq)]
    fn test_exclude_simple() {
        assert_eq!(
            vec![
                FieldRange { low: 1, high: MAX - 1, pos: 0}
            ],
            FieldRange::exclude(
                vec![FieldRange { low: 0, high: MAX - 1, pos: 0}],
                vec![FieldRange { low: 0, high: 0,       pos: 0}]
            ),
            "1"
        );
        assert_eq!(
            vec![
                FieldRange { low: 1, high: 2,       pos: 0},
                FieldRange { low: 4, high: MAX - 1, pos: 0},
            ],
            FieldRange::exclude(
                vec![FieldRange { low: 0, high: MAX - 1, pos: 0}],
                vec![
                    FieldRange { low: 0, high: 0,        pos: 0},
                    FieldRange { low: 3, high: 3,        pos: 0}
                ]
            ),
            "1,4"
        );
        assert_eq!(
            vec![
                FieldRange { low: 2, high: 2,            pos: 0},
            ],
            FieldRange::exclude(
                vec![FieldRange { low: 0, high: MAX - 1, pos: 0}],
                vec![
                    FieldRange { low: 0, high: 1,              pos: 0},
                    FieldRange { low: 3, high: usize::MAX - 1, pos: 1}
                ]
            ),
            "1,2,4-"
        );
        assert_eq!(
            vec![
                FieldRange { low: 0, high: 0,              pos: 0},
            ],
            FieldRange::exclude(
                vec![FieldRange { low: 0, high: MAX - 1, pos: 0}],
                vec![
                    FieldRange { low: 1, high: 2,       pos: 0},
                    FieldRange { low: 3, high: MAX - 1, pos: 1}
                ]
            ),
            "2,3,4-"
        );
        assert_eq!(
            vec![
                FieldRange { low: 1, high: 2,       pos: 0},
            ],
            FieldRange::exclude(
                vec![FieldRange { low: 0, high: MAX - 1, pos: 0}],
                vec![
                    FieldRange { low: 0, high: 0,       pos: 0},
                    FieldRange { low: 3, high: MAX - 1, pos: 1}
                ]
            ),
            "1,4-,5-8"
        );
        assert_eq!(
            vec![
                FieldRange { low: 1, high: 2,       pos: 0},
            ],
            FieldRange::exclude(
                vec![FieldRange { low: 0, high: MAX - 1, pos: 0}],
                vec![
                    FieldRange { low: 0, high: 0,       pos: 1},
                    FieldRange { low: 3, high: MAX - 1, pos: 0},
                    FieldRange { low: 4, high: 7,       pos: 2}
                ]
            ),
            "4-,1,5-8"
        );
        assert_eq!(
            vec![
                FieldRange { low: 4, high: MAX - 1, pos: 0},
            ],
            FieldRange::exclude(
                vec![FieldRange { low: 0, high: MAX - 1, pos: 0}],
                vec![
                    FieldRange { low: 0, high: 3,       pos: 0}
                ]
            ),
            "-4"
        );
        assert_eq!(
            vec![
                FieldRange { low: 8, high: MAX - 1, pos: 0},
            ],
            FieldRange::exclude(
                vec![FieldRange { low: 0, high: MAX - 1, pos: 0}],
                vec![
                    FieldRange { low: 0, high: 7,       pos: 0}
                ]
            ),
            "-4,5-8"
        );
    }
    #[test]
    #[rustfmt::skip::macros(assert_eq)]
    fn test_exclude_complex() {
        assert_eq!(
            vec![
                FieldRange { low: 1, high: 3, pos: 0},
                FieldRange { low: 7, high: 14, pos: 1},
            ],
            FieldRange::exclude(
                vec![FieldRange { low: 0, high: 3, pos: 0}, FieldRange { low: 7, high: MAX - 1, pos: 1}],
                vec![FieldRange { low: 0, high: 0, pos: 0}, FieldRange { low: 15, high: MAX - 1, pos: 0}]
            ),
            "-f1-4,8- : -e1,16-"
        );
        let empty: Vec<FieldRange> = vec![];
        assert_eq!(
            empty,
            FieldRange::exclude(
                vec![FieldRange { low: 0, high: MAX-1, pos: 0}],
                vec![FieldRange { low: 0, high: MAX-1, pos: 0}]
            ),
            "-f1- : -e1-"
        );
        assert_eq!(
            vec![
                FieldRange { low: 0, high: 0, pos: 0},
                FieldRange { low: 9, high: 9, pos: 3},
            ],
            FieldRange::exclude(
                vec![FieldRange { low: 0, high: 0, pos: 0}, FieldRange { low: 3, high: 3, pos: 1 }, FieldRange { low: 7, high: 7, pos: 2}, FieldRange { low: 9, high: 9, pos: 3}],
                vec![FieldRange { low: 3, high: 7, pos: 0}]
            ),
            "-f1,4,8,10 : -e4-8"
        );
        // Fields: XXXXXXXXXXX
        // Exclud:      XXXXXXXXX
        assert_eq!(
            vec![
                FieldRange { low: 0, high: 3, pos: 0},
            ],
            FieldRange::exclude(
                vec![FieldRange { low: 0, high: 9, pos: 0}],
                vec![FieldRange { low: 4, high: MAX - 1, pos: 0}]
            ),
            "-f1-10 : -e5-"
        );
        // Fields:      XXXXXXXXXXXXX
        // Exclud:  XXXXXXXX
        assert_eq!(
            vec![
                FieldRange { low: 15, high: 19, pos: 0},
            ],
            FieldRange::exclude(
                vec![FieldRange { low: 9, high: 19, pos: 0}],
                vec![FieldRange { low: 4, high: 14, pos: 0}]
            ),
            "-f10-20 : -e5-15"
        );
        // Fields: XXXXXXXXXXXXXX
        // Exclud:    XXXXXXX
        assert_eq!(
            vec![
                FieldRange { low: 9, high: 11, pos: 0},
                FieldRange { low: 16, high: 19, pos: 0},
            ],
            FieldRange::exclude(
                vec![FieldRange { low: 9, high: 19, pos: 0}],
                vec![FieldRange { low: 12, high: 15, pos: 0}]
            ),
            "-f10-20 : -e13-16"
        );
        // Fields:      XXX
        // Exclud:    XXXXXXX
        assert_eq!(
            empty,
            FieldRange::exclude(
                vec![FieldRange { low: 12, high: 15, pos: 0}],
                vec![FieldRange { low: 9, high: 19, pos: 0}]
            ),
            "-f13-16 : -e10-20"
        );
        // Fields: XXXXXXXX      XXXXX
        // Exclud:     XXXXXXXXXXXXX
        assert_eq!(
            vec![FieldRange { low: 4, high: 8, pos: 0 }, FieldRange { low: 25, high: 29, pos: 1}],
            FieldRange::exclude(
                vec![FieldRange { low: 4, high: 15, pos: 0}, FieldRange { low: 19, high: 29, pos: 1}],
                vec![FieldRange { low: 9, high: 24, pos: 0}]
            ),
            "-f5-16,20-30 : -e10-25"
        );
    }
}


text:
use crate::field_range::FieldRange;
use bstr::ByteSlice;
use regex::bytes::Regex;
use std::cmp::max;

/// Methods for parsing a line into a reordered `shuffler`
pub trait LineParser<'a> {
    /// Fills the shuffler with values parsed from the line.
    fn parse_line<'b>(&self, line: &'b [u8], shuffler: &mut Vec<Vec<&'b [u8]>>)
    where
        'a: 'b;
}

/// A line parser that works on fixed substrings
pub struct SubStrLineParser<'a> {
    field_ranges: &'a [FieldRange],
    delimiter: &'a [u8],
}

impl<'a> SubStrLineParser<'a> {
    pub fn new(field_ranges: &'a [FieldRange], delimiter: &'a [u8]) -> Self {
        Self {
            field_ranges,
            delimiter,
        }
    }
}
impl<'a> LineParser<'a> for SubStrLineParser<'a> {
    #[inline]
    fn parse_line<'b>(&self, line: &'b [u8], shuffler: &mut Vec<Vec<&'b [u8]>>)
    where
        'a: 'b,
    {
        let mut parts = line.split_str(self.delimiter).peekable();
        let mut iterator_index = 0;

        // Iterate over our ranges and write any fields that are contained by them.
        for &FieldRange { low, high, pos } in self.field_ranges {
            // Advance up to low end of range
            if low > iterator_index {
                match parts.nth(low - iterator_index - 1) {
                    Some(_part) => {
                        iterator_index = low;
                    }
                    None => break,
                }
            }

            // Advance through the range
            for _ in max(low, iterator_index)..=high {
                match parts.next() {
                    Some(part) => {
                        // Guaranteed to be in range since shuffler is created based on field pos anyways
                        if let Some(reshuffled_range) = shuffler.get_mut(pos) {
                            reshuffled_range.push(part)
                        }
                    }
                    None => break,
                }
                iterator_index += 1;
            }
        }
    }
}

/// A line parser that works on fixed substrings
pub struct RegexLineParser<'a> {
    field_ranges: &'a [FieldRange],
    delimiter: &'a Regex,
}

impl<'a> RegexLineParser<'a> {
    pub fn new(field_ranges: &'a [FieldRange], delimiter: &'a Regex) -> Self {
        Self {
            field_ranges,
            delimiter,
        }
    }
}
impl<'a> LineParser<'a> for RegexLineParser<'a> {
    #[inline]
    fn parse_line<'b>(&self, line: &'b [u8], shuffler: &mut Vec<Vec<&'b [u8]>>)
    where
        'a: 'b,
    {
        let mut parts = self.delimiter.split(line).peekable();
        let mut iterator_index = 0;

        // Iterate over our ranges and write any fields that are contained by them.
        for &FieldRange { low, high, pos } in self.field_ranges {
            // Advance up to low end of range
            if low > iterator_index {
                match parts.nth(low - iterator_index - 1) {
                    Some(_part) => {
                        iterator_index = low;
                    }
                    None => break,
                }
            }

            // Advance through the range
            for _ in max(low, iterator_index)..=high {
                match parts.next() {
                    Some(part) => {
                        // Guaranteed to be in range since shuffler is created based on field pos anyways
                        if let Some(reshuffled_range) = shuffler.get_mut(pos) {
                            reshuffled_range.push(part)
                        } else {
                            unreachable!()
                        }
                    }
                    None => break,
                }
                iterator_index += 1;
            }
        }
    }
}


text:
//! [`SingleByteDelimParser`] is a fast mode parser that is to be used when the
//! field separator character is a single byte. It works by using `memchr2` to
//! first look for both the line terminator and the separator in a single pass.
//! Once the furthest right field has been parsed it switches to searching for
//! just newlines.
use std::{
    cmp::min,
    io::{self, Write},
};

use ripline::LineTerminator;

use crate::{core::JoinAppend, field_range::FieldRange};

/// A `SingleByteDelimParser` is a fast parser of fields from from a buffer.
pub struct SingleByteDelimParser<'a> {
    /// newline aligned buffer, must end in newline
    line_terminator: LineTerminator,
    output_delimiter: &'a [u8],
    fields: &'a [FieldRange],
    sep: u8,
    /// The furthers right field
    max_field: usize,
    /// Current offset into the buffer
    offset: usize,
    newline: u8,
    line: Vec<(usize, usize)>,
}

impl<'a> SingleByteDelimParser<'a> {
    /// Create a [`SingleByteDelimParser`] to process buffers using the input configuration.
    pub fn new(
        line_terminator: LineTerminator,
        output_delimiter: &'a [u8],
        fields: &'a [FieldRange],
        sep: u8,
    ) -> Self {
        Self {
            line_terminator,
            output_delimiter,
            fields,
            sep,
            max_field: fields.last().map_or(usize::MAX, |f| f.high + 1),
            offset: 0,
            newline: line_terminator.as_byte(),
            line: vec![],
        }
    }

    /// Clear all fields of the [`SingleByteDelimParser`].
    #[inline]
    pub fn reset(&mut self) {
        self.offset = 0;
    }

    /// Parse fields from the lines found in buffer and write them to `output`.
    ///
    /// **Note** The input buffer _must_ end with a newline.
    #[inline]
    pub fn process_buffer<W: Write>(
        &mut self,
        buffer: &[u8],
        mut output: W,
    ) -> Result<(), io::Error> {
        // Advance pasts first newline
        if let Some(byte) = buffer.first() {
            if *byte == self.newline {
                output.join_append(
                    self.output_delimiter,
                    std::iter::empty(),
                    &self.line_terminator,
                )?;
                self.offset += 1;
            }
        }

        while self.offset < buffer.len() {
            self.fill_line(buffer)?;
            let items = self.fields.iter().flat_map(|f| {
                let slice = self
                    .line
                    .get(f.low..=min(f.high, self.line.len().saturating_sub(1)))
                    .unwrap_or(&[]);
                slice.iter().map(|(start, stop)| &buffer[*start..=*stop])
            });
            output.join_append(self.output_delimiter, items, &self.line_terminator)?;
            self.line.clear();
        }
        Ok(())
    }

    /// Fill `line` with the start/end positions of found columns
    /// The positions are relative to the held buffer
    #[inline]
    fn fill_line(&mut self, buffer: &[u8]) -> Result<(), io::Error> {
        let mut field_count = 0;
        let iter = memchr::memchr2_iter(self.sep, self.newline, &buffer[self.offset..]);

        let mut line_offset = 0;
        let mut found_newline = false;

        for index in iter {
            if buffer[self.offset + index] == self.sep {
                field_count += 1;
            } else {
                found_newline = true;
            }

            self.line
                .push((self.offset + line_offset, self.offset + index - 1));
            line_offset = index + 1;

            if found_newline || field_count == self.max_field {
                break;
            }
        }

        if !found_newline {
            let end = memchr::memchr(self.newline, &buffer[self.offset + line_offset..])
                .ok_or(io::ErrorKind::InvalidData)?;
            self.offset += line_offset + end + 1;
        } else {
            self.offset += line_offset;
        }
        Ok(())
    }
}


text:
fs      = require("fs")
watcher = require("node-watch")

stitch  = require("stitch")
stylus  = require("stylus")

buildScripts = ->
  IN_DIR   = "./src"
  OUT_FILE = "compiled/app.js"

  build = ->
    # Stitch
    pkg = stitch.createPackage({paths: [IN_DIR]})
    pkg.compile (err, output) ->
      if err
        console.warn err
        return

      fs.writeFile OUT_FILE, output, (err) ->
        if err then throw err
        console.log "compiled: #{OUT_FILE}"

    # Stylus
    fs.readFile "./src/style/style.styl", "utf8", (err, data) ->
      stylus.render data, (err, css) ->
        fs.writeFile "compiled/app.css", css, (err) ->
          if err then throw err
          console.log "compiled: compiled/app.css"

  build()
  watcher(IN_DIR, build)

buildScripts()

repo_name: cdglabs/Shadershop

repo_language: JavaScript


text:
Changes to presentation
  Have a small explainer on composition and ordering
    Show Compose Sine Fract and Compose Fract Sine
  Maybe have a slide showing overview of the noise function construction process steps (i.e. custom functions)
    Random, Step Random, Noise, Fractal Noise

Changes to system
  Rotate modifier
  Snap on/off modifier
  When you create a new fn, give it the plot of the currently editing fn

Architecture
  Move "builtIn" into its own file.
  Move "combiners" into their own thing. Think of better name.

Misc
  Figure out how React context works
  Do something better than util.safeInv
  Tune mousewheel zooming
  Delete key
  Undo
  Duplicate
  Check that you don't create dependency loops
  Need to listen for scroll events on Scrollers and refresh

Dirty system
  Should have a thing for keeping track of whether certain things might be dirty, ala the Compiler.

Plot
  Better "settings" controls
  Clean up hard coded stuff in plot layout
  ColorMap needs to be dimensionally generic

Outline Stuff
  Create definition button / right click item - create by abstracting
  Show transform numbers when they're not identity?

Inspector
  Highlight transform numbers when hovering the control point
  Show transform numbers next to control point
  Scrub transform numbers in inspector?

Palette
  Insert button
  Anything can be dragged into the outline to put it in a specific place
  Maybe
    + insert buttons in outline like before
  Some way to delete definitions

Nice to have
  flexible color scale for colorMap

  rotate/scale modifier when dragging transform points

  performance
    send in selectedChildFn transforms as uniforms into shaders to make dragging control points faster

  highlight hovered/active transform variables

  spread based on the hovered transform point/variable(s)

  maybe a hover in "window" view for colorMap

  look into changing data model / API for childFn to make control point code less ugly


text:
<!DOCTYPE html>

<title>Shadershop</title>

<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<link rel="apple-touch-icon" href="assets/icon.png" />
<link rel="stylesheet" href="compiled/app.css" type="text/css" media="screen" charset="utf-8" />
<link rel="stylesheet" href="assets/fontello/css/shadershopfont.css" type="text/css" media="screen" charset="utf-8" />


text:
<!DOCTYPE html>

<title>Shadershop Development</title>

<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<link rel="apple-touch-icon" href="assets/icon.png" />
<link rel="stylesheet" href="compiled/app.css" type="text/css" media="screen" charset="utf-8" />
<link rel="stylesheet" href="assets/fontello/css/shadershopfont.css" type="text/css" media="screen" charset="utf-8" />


text:
# Shadershop

http://tobyschachman.com/Shadershop/

Shadershop is an interface for programming GPU shaders in the mode of a direct manipulation image editor like Photoshop. It is an experiment in leveraging the programmer’s spatial reasoning the way that coding today leverages the programmer’s symbolic reasoning.

## Demo

http://www.cdglabs.org/Shadershop/

### Keyboard Commands

* Ctrl-R to reset. Shadershop will auto-save.
* Ctrl-1 through Ctrl-4 to load examples from the videos.
* Shift-click will also work to select multiple functions.

## Build Instructions

Install [node.js](http://nodejs.org/) and [coffeescript](http://coffeescript.org/).

Run `npm install` in this directory to install the development dependencies.

Run `coffee build.coffee` to build. It will compile the files in `src` into `compiled/app.js` and `compiled/app.css`. It will also automatically watch for changes in `src` files and recompile as necessary.

Now just open `index.html`. You can also open `dev.html` which uses the development version of React and will give you better console warnings.


text:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>app</key>
	<string>com.bohemiancoding.sketch</string>
	<key>build</key>
	<integer>5355</integer>
	<key>commit</key>
	<string>b7d299b0a34651d1a0e066786b75aa36168d5809</string>
	<key>fonts</key>
	<array/>
	<key>length</key>
	<integer>11787</integer>
	<key>version</key>
	<integer>18</integer>
</dict>
</plist>


text:
<!DOCTYPE html>

    <meta charset="UTF-8">
    <link rel="stylesheet" href="css/shadershopfont.css">
    <link rel="stylesheet" href="css/animation.css">

        shadershopfont
         font demo

        <input type="checkbox" onclick="toggleCodes(this.checked)">show codes

 icon-eye0xe800

 icon-pencil0xe801

 icon-list-add0xe802

 icon-cog0xe803

 icon-resize-full-alt0xe804

 icon-move0xe805

Generated by fontello.com


text:
This webfont is generated by http://fontello.com open source project.

================================================================================
Please, note, that you should obey original font licences, used to make this
webfont pack. Details available in LICENSE.txt file.

- Usually, it's enough to publish content of LICENSE.txt file somewhere on your
  site in "About" section.

- If your project is open-source, usually, it will be ok to make LICENSE.txt
  file publically available in your repository.

- Fonts, used in Fontello, don't require to make clickable links on your site.
  But any kind of additional authors crediting is welcome.
================================================================================

Comments on archive content
---------------------------

- /font/* - fonts in different formats

- /css/*  - different kinds of css, for all situations. Should be ok with
  twitter bootstrap. Also, you can skip <i> style and assign icon classes
  directly to text elements, if you don't mind about IE7.

- demo.html - demo file, to show your webfont content

- LICENSE.txt - license info about source fonts, used to build your one.

- config.json - keeps your settings. You can import it back to fontello anytime,
  to continue your work

Why so many CSS files ?
-----------------------

Because we like to fit all your needs :)

- basic file, <your_font_name>.css - is usually enougth, in contains @font-face
  and character codes definition

- *-ie7.css - if you need IE7 support, but still don't wish to put char codes
  directly into html

- *-codes.css and *-ie7-codes.css - if you like to use your own @font-face
  rules, but still wish to benefit of css generation. That can be very
  convenient for automated assets build systems. When you need to update font -
  no needs to manually edit files, just override old version with archive
  content. See fontello source codes for example.

- *-embedded.css - basic css file, but with embedded WOFF font, to avoid
  CORS issues in Firefox and IE9+, when fonts are hosted on the separate domain.
  We strongly recommend to resolve this issue by `Access-Control-Allow-Origin`
  server headers. But if you ok with dirty hack - this file is for you. Note,
  that data url moved to separate @font-face to avoid problems with <IE9, when
  string is too long.

- animate.css - use it to get ideas about spinner rotation animation.

Attention for server setup
--------------------------

You MUST setup server to reply with proper `mime-types` for font files. In other
case, some browsers will fail to show fonts.

Usually, `apache` already has necessary settings, but `nginx` and other
webservers should be tuned. Here is list of mime types for our file extentions:

- `application/vnd.ms-fontobject` - eot
- `application/x-font-woff` - woff
- `application/x-font-ttf` - ttf
- `image/svg+xml` - svg


text:
.icon-eye:before { content: '\e800'; } /* '' */
.icon-pencil:before { content: '\e801'; } /* '' */
.icon-list-add:before { content: '\e802'; } /* '' */
.icon-cog:before { content: '\e803'; } /* '' */
.icon-resize-full-alt:before { content: '\e804'; } /* '' */
.icon-move:before { content: '\e805'; } /* '' */


text:
@font-face {
  font-family: 'shadershopfont';
  src: url('../font/shadershopfont.eot?72703266');
  src: url('../font/shadershopfont.eot?72703266#iefix') format('embedded-opentype'),
       url('../font/shadershopfont.woff?72703266') format('woff'),
       url('../font/shadershopfont.ttf?72703266') format('truetype'),
       url('../font/shadershopfont.svg?72703266#shadershopfont') format('svg');
  font-weight: normal;
  font-style: normal;
}
/* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */
/* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */
/*
@media screen and (-webkit-min-device-pixel-ratio:0) {
  @font-face {
    font-family: 'shadershopfont';
    src: url('../font/shadershopfont.svg?72703266#shadershopfont') format('svg');
  }
}
*/

 [class^="icon-"]:before, [class*=" icon-"]:before {
  font-family: "shadershopfont";
  font-style: normal;
  font-weight: normal;
  speak: none;

  display: inline-block;
  text-decoration: inherit;
  width: 1em;
  margin-right: .2em;
  text-align: center;
  /* opacity: .8; */

  /* For safety - reset parent styles, that can break glyph codes*/
  font-variant: normal;
  text-transform: none;

  /* fix buttons height, for twitter bootstrap */
  line-height: 1em;

  /* Animation center compensation - margins should be symmetric */
  /* remove if not needed */
  margin-left: .2em;

  /* you can be more comfortable with increased icons size */
  /* font-size: 120%; */

  /* Uncomment for 3D effect */
  /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */
}

.icon-eye:before { content: '\e800'; } /* '' */
.icon-pencil:before { content: '\e801'; } /* '' */
.icon-list-add:before { content: '\e802'; } /* '' */
.icon-cog:before { content: '\e803'; } /* '' */
.icon-resize-full-alt:before { content: '\e804'; } /* '' */
.icon-move:before { content: '\e805'; } /* '' */


text:
/*
   Animation example, for spinners
*/
.animate-spin {
  -moz-animation: spin 2s infinite linear;
  -o-animation: spin 2s infinite linear;
  -webkit-animation: spin 2s infinite linear;
  animation: spin 2s infinite linear;
  display: inline-block;
}
@-moz-keyframes spin {
  0% {
    -moz-transform: rotate(0deg);
    -o-transform: rotate(0deg);
    -webkit-transform: rotate(0deg);
    transform: rotate(0deg);
  }

  100% {
    -moz-transform: rotate(359deg);
    -o-transform: rotate(359deg);
    -webkit-transform: rotate(359deg);
    transform: rotate(359deg);
  }
}
@-webkit-keyframes spin {
  0% {
    -moz-transform: rotate(0deg);
    -o-transform: rotate(0deg);
    -webkit-transform: rotate(0deg);
    transform: rotate(0deg);
  }

  100% {
    -moz-transform: rotate(359deg);
    -o-transform: rotate(359deg);
    -webkit-transform: rotate(359deg);
    transform: rotate(359deg);
  }
}
@-o-keyframes spin {
  0% {
    -moz-transform: rotate(0deg);
    -o-transform: rotate(0deg);
    -webkit-transform: rotate(0deg);
    transform: rotate(0deg);
  }

  100% {
    -moz-transform: rotate(359deg);
    -o-transform: rotate(359deg);
    -webkit-transform: rotate(359deg);
    transform: rotate(359deg);
  }
}
@-ms-keyframes spin {
  0% {
    -moz-transform: rotate(0deg);
    -o-transform: rotate(0deg);
    -webkit-transform: rotate(0deg);
    transform: rotate(0deg);
  }

  100% {
    -moz-transform: rotate(359deg);
    -o-transform: rotate(359deg);
    -webkit-transform: rotate(359deg);
    transform: rotate(359deg);
  }
}
@keyframes spin {
  0% {
    -moz-transform: rotate(0deg);
    -o-transform: rotate(0deg);
    -webkit-transform: rotate(0deg);
    transform: rotate(0deg);
  }

  100% {
    -moz-transform: rotate(359deg);
    -o-transform: rotate(359deg);
    -webkit-transform: rotate(359deg);
    transform: rotate(359deg);
  }
}


text:
.icon-eye { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
.icon-pencil { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
.icon-list-add { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
.icon-cog { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
.icon-resize-full-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
.icon-move { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }


text:
[class^="icon-"], [class*=" icon-"] {
  font-family: 'shadershopfont';
  font-style: normal;
  font-weight: normal;

  /* fix buttons height */
  line-height: 1em;

  /* you can be more comfortable with increased icons size */
  /* font-size: 120%; */
}

.icon-eye { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
.icon-pencil { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
.icon-list-add { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
.icon-cog { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
.icon-resize-full-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
.icon-move { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }


text:
require("./util/util")
require("./config")
require("./model/C")
require("./Actions")
require("./Compiler")
require("./view/R")
require("./keyCommands")

storageName = config.storageName

isReloading = false
reloadPage = ->
  isReloading = true
  location.reload()

window.reset = ->
  delete window.localStorage[storageName]
  reloadPage()

if json = window.localStorage[storageName]
  json = JSON.parse(json)
  window.appRoot = C.reconstruct(json)
else
  window.appRoot = new C.AppRoot()

saveState = ->
  return if isReloading # so we don't overwrite while reloading
  json = C.deconstruct(appRoot)
  json = JSON.stringify(json)
  window.localStorage[storageName] = json

window.save = ->
  window.localStorage[storageName]

window.restore = (jsonString) ->
  if !_.isString(jsonString)
    jsonString = JSON.stringify(jsonString)
  window.localStorage[storageName] = jsonString
  reloadPage()

require("./UI")

# =============================================================================
# Animate Loop
# =============================================================================

debouncedSaveState = _.debounce(saveState, 400)

willRefreshNextFrame = false
refresh = ->
  return if willRefreshNextFrame
  willRefreshNextFrame = true
  requestAnimationFrame ->
    refreshView()
    # saveState()
    debouncedSaveState()
    willRefreshNextFrame = false

refreshView = ->
  appRootEl = document.querySelector("#AppRoot")
  React.renderComponent(R.AppRootView({appRoot}), appRootEl)

refreshEventNames = [
  "mousedown"
  "mousemove"
  "mouseup"
  "keydown"
  "scroll"
  "change"
  "wheel"
  "mousewheel"
]

for eventName in refreshEventNames
  window.addEventListener(eventName, refresh)

refresh()

# =============================================================================
# Auto-reload stylesheet
# =============================================================================

# Firefox crashes when the stylesheet reloads.
if location.protocol == "file:" and navigator.userAgent.indexOf("Firefox") == -1 and location.href.indexOf("dev.html") != -1
  setInterval(->
    document.styleSheets[0].reload()
  , 1000)


text:
mixColors = (c1, c2, amount) ->
  amount1 = 1 - amount
  amount2 = amount
  numeric.add(numeric.mul(amount1, c1), numeric.mul(amount2, c2))

mainColor = [0.2, 0.2, 0.2, 1]
childColor = [0.8, 0.8, 0.8, 1]
selectedColor = [0, 0.6, 0.8, 1]
hoveredColor = mixColors(childColor, selectedColor, 0.3)

window.config = config = {

  storageName: "sinewaves"

  resolution: 0.5
  dimensions: 4

  mainLineWidth: 1.25

  # In pixels:
  minGridSpacing: 90
  hitTolerance: 10
  snapTolerance: 7

  outlineIndent: 16

  gridColor: "204,194,163"
  domainLabelColor: "#900"
  rangeLabelColor: "#090"

  # colorMapPositive: ".80, .69, .36"
  # colorMapNegative: ".36, .47, .80"
  # colorMapPositive: ".84, .75, .47"
  # colorMapNegative: ".47, .56, .84"

  # colorMapZero:     util.hslToRgb(0, 0, 0.5).map(util.glslString).join(",")
  # colorMapPositive: util.hslToRgb(46, 1, 0.85).map(util.glslString).join(",")
  # colorMapNegative: util.hslToRgb(226, 1, 0.15).map(util.glslString).join(",")

  colorMapZero:     util.hslToRgb(0, 0, 0).map(util.glslString).join(",")
  colorMapPositive: util.hslToRgb(0, 0, 1).map(util.glslString).join(",")
  colorMapNegative: util.hslToRgb(226, 1, 0.4).map(util.glslString).join(",")

  style: {
    main: {
      strokeStyle: "#333"
      lineWidth: 1.25
    }
    default: {
      strokeStyle: "#ccc"
      lineWidth: 1.25
    }
    selected: {
      strokeStyle: "#09c"
      lineWidth: 1.25
    }
  }

  color: {
    main: mainColor
    child: childColor
    selected: selectedColor
    hovered: hoveredColor
  }

  cursor: {
    text: "text"
    grab: "-webkit-grab"
    grabbing: "-webkit-grabbing"
    verticalScrub: "ns-resize"
    horizontalScrub: "ew-resize"
  }

}


text:
window.Actions = Actions = {}

# =============================================================================
# Manipulating Fn hierarchy
# =============================================================================

findParentIndexOf = (childFnTarget) ->
  recurse = (compoundFn) ->
    index = compoundFn.childFns.indexOf(childFnTarget)
    if index != -1
      return {
        parent: compoundFn
        index: index
      }

    for childFn in compoundFn.childFns
      if childFn.fn instanceof C.CompoundFn
        if found = recurse(childFn.fn)
          return found

    return null
  recurse(UI.selectedFn)

getExpandedChildFns = ->
  result = []
  recurse = (childFns) ->
    for childFn in childFns
      continue unless childFn.visible
      result.push(childFn)
      if UI.isChildFnExpanded(childFn) and childFn.fn instanceof C.CompoundFn
        recurse(childFn.fn.childFns)
  recurse(UI.selectedFn.childFns)
  return result

ensureSelectedChildFnsVisible = ->
  expandedChildFns = getExpandedChildFns()
  UI.selectedChildFns = _.intersection(UI.selectedChildFns, expandedChildFns)

Actions.addDefinedFn = ->
  fn = new C.DefinedFn()
  appRoot.fns.push(fn)
  Compiler.setDirty()
  Actions.selectFn(fn)

Actions.addChildFn = (fn) ->
  if UI.selectedChildFns.length == 1
    possibleParent = UI.selectedChildFns[0].fn
    takesChildren = (possibleParent instanceof C.CompoundFn and UI.isChildFnExpanded(UI.selectedChildFns[0]))
    if takesChildren
      parent = possibleParent
      index = parent.childFns.length
    else
      {parent, index} = findParentIndexOf(UI.selectedChildFns[0])
      index = index + 1 # insert it after the selectedChildFn

  else
    if UI.selectedFn.childFns.length == 1
      onlyChild = UI.selectedFn.childFns[0]
      possibleParent = onlyChild.fn
      takesChildren = (possibleParent instanceof C.CompoundFn and UI.isChildFnExpanded(onlyChild))
      if takesChildren
        parent = possibleParent
        index = parent.childFns.length

  if !parent?
    parent = UI.selectedFn
    index = parent.childFns.length

  if createsCircularDependency(fn, parent)
    # TODO: display a warning in the UI
    console.warn "Creates circular dependency"
    return

  childFn = new C.ChildFn(fn)
  Actions.insertChildFn(parent, childFn, index)
  Actions.selectChildFn(childFn)
  return {childFn, parent, index}

createsCircularDependency = (fnToAdd, existingFn) ->
  # The function being added would create a circular depedency if it containts
  # its soon-to-be parent anywhere in its tree.
  if fnToAdd == existingFn
    return true
  else if fnToAdd instanceof C.CompoundFn
    return _.any fnToAdd.childFns, (childFn) ->
      return createsCircularDependency(childFn, existingFn)
  else if fnToAdd instanceof C.ChildFn
    return createsCircularDependency(fnToAdd.fn, existingFn)
  else # C.BuiltInFn
    return false

Actions.addCompoundFn = (combiner="sum") ->
  compoundFn = new C.CompoundFn()
  compoundFn.combiner = combiner
  compoundFnContainer = new C.ChildFn(compoundFn)

  # Figure out where to add it
  childFnInfos = []
  for childFn in UI.selectedChildFns
    {parent, index} = findParentIndexOf(childFn)
    childFnInfos.push {parent, index, childFn}

  if childFnInfos.length > 0
    commonParent = childFnInfos[0].parent
    if _.all(childFnInfos, (childFnInfo) -> childFnInfo.parent == commonParent)
      # UI.selectedChildFns meets all criteria to be wrapped
      childFnInfos = _.sortBy(childFnInfos, "index")

      # Remove them from current location and into compoundFn
      for childFnInfo in childFnInfos
        Actions.removeChildFn(commonParent, childFnInfo.childFn)
        compoundFn.childFns.push(childFnInfo.childFn)

      # Insert compoundFnContainer
      index = childFnInfos[0].index
      commonParent.childFns.splice(index, 0, compoundFnContainer)

      Actions.selectChildFn(compoundFnContainer)
      return

  # UI.selectedChildFns did not meet the criteria, wrap the compoundFn around
  # everything.

  compoundFn.childFns = UI.selectedFn.childFns
  UI.selectedFn.childFns = [compoundFnContainer]
  Actions.selectChildFn(compoundFnContainer)
  Compiler.setDirty()

Actions.removeChildFn = (parentCompoundFn, childFn) ->
  index = parentCompoundFn.childFns.indexOf(childFn)
  return if index == -1
  parentCompoundFn.childFns.splice(index, 1)
  ensureSelectedChildFnsVisible()
  Compiler.setDirty()

Actions.insertChildFn = (parentCompoundFn, childFn, index) ->
  parentCompoundFn.childFns.splice(index, 0, childFn)
  Compiler.setDirty()

# =============================================================================
# Manipulating Fn incidentals
# =============================================================================

Actions.setFnLabel = (fn, newValue) ->
  fn.label = newValue

Actions.setCompoundFnCombiner = (compoundFn, combiner) ->
  compoundFn.combiner = combiner
  Compiler.setDirty()

Actions.setVariableValueString = (variable, newValueString) ->
  variable.valueString = newValueString
  Compiler.setDirty()

Actions.toggleChildFnVisible = (childFn) ->
  Actions.setChildFnVisible(childFn, !childFn.visible)

Actions.setChildFnVisible = (childFn, newVisible) ->
  childFn.visible = newVisible
  ensureSelectedChildFnsVisible()
  Compiler.setDirty()

Actions.setBasisVector = (childFn, space, coord, valueStrings) ->
  childFn.setBasisVector(space, coord, valueStrings)
  Compiler.setDirty()

# =============================================================================
# Manipulating Plots
# =============================================================================

Actions.panPlot = (plot, from, to) ->
  offset = numeric.sub(from, to)
  plot.center = numeric.add(plot.center, offset)

Actions.zoomPlot = (plot, zoomCenter, scaleFactor) ->
  # scaleFactor > 1 is zoom "out"

  offset = numeric.sub(plot.center, zoomCenter)
  plot.center = numeric.add(
    zoomCenter
    numeric.mul(offset, scaleFactor)
  )
  plot.pixelSize *= scaleFactor

# HACK: This duplicates information but I'll do it until I figure out a proper
# data model for PlotLayout, Plot, etc.

Actions.panPlotLayout = (plotLayout, from, to) ->
  for plot in plotLayout.plots
    Actions.panPlot(plot, from, to)

Actions.zoomPlotLayout = (plotLayout, zoomCenter, scaleFactor) ->
  for plot in plotLayout.plots
    Actions.zoomPlot(plot, zoomCenter, scaleFactor)

Actions.setPlotLayoutFocus = (plotLayout, focus) ->
  for plot in plotLayout.plots
    plot.focus = focus

# =============================================================================
# Changing UI state (selection, hover, expanded)
# =============================================================================

Actions.selectFn = (fn) ->
  return unless fn instanceof C.DefinedFn
  UI.selectedFn = fn
  UI.selectedChildFns = []
  Compiler.setDirty()

Actions.selectChildFn = (childFn) ->
  if childFn == null
    UI.selectedChildFns = []
  else
    UI.selectedChildFns = [childFn]
  ensureSelectedChildFnsVisible()
  Compiler.setDirty()

Actions.toggleSelectChildFn = (childFn) ->
  if _.contains(UI.selectedChildFns, childFn)
    UI.selectedChildFns = _.without(UI.selectedChildFns, childFn)
  else
    UI.selectedChildFns.push(childFn)
  ensureSelectedChildFnsVisible()
  Compiler.setDirty()

Actions.hoverChildFn = (childFn) ->
  UI.hoveredChildFn = childFn

Actions.toggleChildFnExpanded = (childFn) ->
  expanded = UI.isChildFnExpanded(childFn)
  Actions.setChildFnExpanded(childFn, !expanded)

Actions.setChildFnExpanded = (childFn, expanded) ->
  id = C.id(childFn)
  UI.expandedChildFns[id] = expanded
  ensureSelectedChildFnsVisible()
  Compiler.setDirty()


text:
window.UI = UI = new class
  constructor: ->
    @dragging = null
    @mousePosition = {x: 0, y: 0}
    @autofocus = null

    @selectedFn = _.last(appRoot.fns)

    @selectedChildFns = []
    @hoveredChildFn = null

    @expandedChildFns = {}

    @showSymbolic = true

    @registerEvents()

  registerEvents: ->
    window.addEventListener("mousemove", @handleWindowMouseMove)
    window.addEventListener("mouseup", @handleWindowMouseUp)

  # ===========================================================================
  # Helpers
  # ===========================================================================

  isChildFnExpanded: (childFn) ->
    id = C.id(childFn)
    expanded = @expandedChildFns[id]
    if !expanded?
      if childFn.fn instanceof C.DefinedFn
        return false
      else
        return true
    return expanded

  getSingleSelectedChildFn: ->
    if @selectedChildFns.length == 1
      return @selectedChildFns[0]
    else
      return null

  # ===========================================================================
  # Dragging and Mouse Position
  # ===========================================================================

  handleWindowMouseMove: (e) =>
    @mousePosition = {x: e.clientX, y: e.clientY}
    @dragging?.onMove?(e)

  handleWindowMouseUp: (e) =>
    @dragging?.onUp?(e)
    @dragging = null
    if @hoverIsActive
      @hoverData = null
      @hoverIsActive = false

  getElementUnderMouse: ->
    draggingOverlayEl = document.querySelector(".draggingOverlay")
    draggingOverlayEl?.style.pointerEvents = "none"

    el = document.elementFromPoint(@mousePosition.x, @mousePosition.y)

    draggingOverlayEl?.style.pointerEvents = ""

    return el

  getViewUnderMouse: ->
    el = @getElementUnderMouse()
    el = el?.closest (el) -> el.dataFor?
    return el?.dataFor


text:
###

The idea here is that we're creating intermediate representations of each fn,
currently consisting of exprString:String and dependencies:[DefinedFn]. The
intermediate representation is created by getExprStringAndDependencies.

We currently cache each intermediate representation, and throw away the entire
cache if any function changes.

Cacheing is important because:

We can determine both the exprString and dependencies in a single pass, but we
usually only need (care to think about) one of these at a time.

We need to refer to the dependencies very frequently in order to compute
allDependencies. We don't want to repeat the work of figuring out
dependencies.

We need the exprString of a Fn for every Fn that is recursively dependent on
it.

What invalidates the cache, that is, what makes a Fn's intermediate
representation dirty?

What else do we need to add to the intermediate representation?

Dependencies on uniforms for scrubbing optimization. If only one ChildFn is
selected, then it will use uniform variables as its translate/transform
vectors/matrices so as to not have to recompile as the control points are
dragged around.

Dependencies on textures for ImageFn's.

What are other uses for intermediate representation?

Human readable "compiler".

What else might want to use this cache strategy?

Variables already kind of use it. We cache the (numeric) value of the Variable
even though it is derived from the stringValue (though kind of not, since an
invalid stringValue results in the last working value). We dirty check that by
just comparing is stringValue is the same as the last time variable.getValue()
was called.

The matrices/vectors in ChildFn probably want to do their own cacheing. Right
now they have to put together their values by getting the value of each
variable, which seems (?) expensive just because there are so many variables.
Also the matrix inversion function needed for the "divide" in domainTransform
is expensive and ought to be cached.

###

window.Compiler = Compiler = {}

# Constants
vecType = util.glslVectorType(config.dimensions)
zeroVectorString = util.glslString(util.constructVector(config.dimensions, 0))
identityMatrixString = util.glslString(numeric.identity(config.dimensions))

cache = {} # ID : {exprString, dependencies}

Compiler.setDirty = ->
  cache = {}

getExprStringAndDependencies = (fn) ->
  id = C.id(fn)
  return cache[id] if cache[id]?

  dependencies = []
  recurse = (fn, parameter, firstCall=false) ->
    if fn instanceof C.BuiltInFn
      if fn.fnName == "identity"
        return parameter
      return "#{fn.fnName}(#{parameter})"

    if fn instanceof C.DefinedFn and !firstCall
      dependencies.push(fn)
      return "#{C.id(fn)}(#{parameter})"

    if fn instanceof C.CompoundFn
      visibleChildFns = _.filter fn.childFns, (childFn) -> childFn.visible

      if fn.combiner == "last"
        if visibleChildFns.length > 0
          return recurse(_.last(visibleChildFns), parameter)
        else
          return util.glslString(util.constructVector(config.dimensions, 0))

      if fn.combiner == "composition"
        exprString = parameter
        for childFn in visibleChildFns
          exprString = recurse(childFn, exprString)
        return exprString

      childExprStrings = visibleChildFns.map (childFn) =>
        recurse(childFn, parameter)

      if fn.combiner == "sum"
        if childExprStrings.length == 0
          return util.glslString(util.constructVector(config.dimensions, 0))
        else
          return "(" + childExprStrings.join(" + ") + ")"

      if fn.combiner == "product"
        if childExprStrings.length == 0
          return util.glslString(util.constructVector(config.dimensions, 1))
        else
          return "(" + childExprStrings.join(" * ") + ")"

      if fn.combiner == "min" or fn.combiner == "max"
        if childExprStrings.length == 0
          return util.glslString(util.constructVector(config.dimensions, 0))
        else
          reducer = (rest, expr) ->
            if rest == ""
              expr
            else
              "#{fn.combiner}(#{rest}, #{expr})"
          return _.reduce(childExprStrings, reducer, "")

    if fn instanceof C.ChildFn
      if fn == UI.getSingleSelectedChildFn()
        exprString = parameter
        exprString = "selectedDomainTransformInv * (#{exprString} - selectedDomainTranslate)"
        exprString = recurse(fn.fn, exprString)
        exprString = "((selectedRangeTransform * #{exprString}) + selectedRangeTranslate)"
        return exprString

      domainTranslate    = util.glslString(fn.getDomainTranslate())
      domainTransformInv = util.glslString(util.safeInv(fn.getDomainTransform()))
      rangeTranslate     = util.glslString(fn.getRangeTranslate())
      rangeTransform     = util.glslString(fn.getRangeTransform())

      exprString = parameter

      if domainTranslate != zeroVectorString
        exprString = "(#{exprString} - #{domainTranslate})"

      if domainTransformInv != identityMatrixString
        exprString = "(#{domainTransformInv} * #{exprString})"

      exprString = recurse(fn.fn, exprString)

      if rangeTransform != identityMatrixString
        exprString = "(#{rangeTransform} * #{exprString})"

      if rangeTranslate != zeroVectorString
        exprString = "(#{exprString} + #{rangeTranslate})"

      return exprString

  exprString = recurse(fn, "inputVal", true)

  result = {exprString, dependencies}

  cache[id] = result
  return result

getDependencies = (fn) ->
  return getExprStringAndDependencies(fn).dependencies

getAllDependencies = (fn) ->
  # Returns all dependencies in order e.g. [A, B] then A does not depend on B,
  # so the dependencies can be listed in order in code without a problem.

  allDependencies = []
  recurse = (fn) ->
    dependencies = getDependencies(fn)
    allDependencies = allDependencies.concat(dependencies)
    for dependency in dependencies
      recurse(dependency)
  recurse(fn)

  allDependencies.reverse()
  allDependencies = _.unique(allDependencies)

  return allDependencies

getGlslFnString = (fn, name) ->
  name ?= C.id(fn)
  {exprString} = getExprStringAndDependencies(fn)
  return "#{vecType} #{name}(#{vecType} inputVal) {return #{exprString};}"

Compiler.getGlsl = (fn) ->
  {exprString} = getExprStringAndDependencies(fn)
  allDependencies = getAllDependencies(fn)

  fnStrings = (getGlslFnString(dependency) for dependency in allDependencies)

  fnStrings.push getGlslFnString(fn, "mainFn")

  return fnStrings.join("\n")


text:
lerp = util.lerp

devicePixelRatio = window.devicePixelRatio || 1 # Scale canvas for HiDPI displays

canvasBounds = (ctx) ->
  canvas = ctx.canvas
  canvas.width *= devicePixelRatio
  canvas.height *= devicePixelRatio
  {
    cxMin: 0
    cxMax: canvas.width
    cyMin: canvas.height
    cyMax: 0
    width: canvas.width
    height: canvas.height
  }

clear = (ctx) ->
  canvas = ctx.canvas
  ctx.clearRect(0, 0, canvas.width, canvas.height)

# =============================================================================
# Grid
# =============================================================================

ticks = (spacing, min, max) ->
  first = Math.ceil(min / spacing)
  last = Math.floor(max / spacing)
  (x * spacing for x in [first..last])

drawLine = (ctx, [x1, y1], [x2, y2]) ->
  ctx.beginPath()
  ctx.moveTo(x1, y1)
  ctx.lineTo(x2, y2)
  ctx.stroke()

getSpacing = (pixelSize) ->
  # minSpacing is the minimum distance, in world coordinates, between major
  # grid lines.
  minSpacing = pixelSize * config.minGridSpacing

  ###
  need to determine:
    largeSpacing = {1, 2, or 5} * 10^n
    smallSpacing = divide largeSpacing by 4 (if 1 or 2) or 5 (if 5)
  largeSpacing must be greater than minSpacing
  ###
  div = 4
  largeSpacing = z = Math.pow(10, Math.ceil(Math.log(minSpacing) / Math.log(10)))
  if z / 5 > minSpacing
    largeSpacing = z / 5
  else if z / 2 > minSpacing
    largeSpacing = z / 2
    div = 5
  smallSpacing = largeSpacing / div

  return {largeSpacing, smallSpacing}

drawGrid = (ctx, opts) ->
  xMin = opts.xMin
  xMax = opts.xMax
  yMin = opts.yMin
  yMax = opts.yMax
  pixelSize = opts.pixelSize
  xLabel = opts.xLabel
  yLabel = opts.yLabel
  xLabelColor = opts.xLabelColor
  yLabelColor = opts.yLabelColor

  {cxMin, cxMax, cyMin, cyMax, width, height} = canvasBounds(ctx)

  {largeSpacing, smallSpacing} = getSpacing(pixelSize)

  toLocal = ([cx, cy]) ->
    [
      lerp(cx, cxMin, cxMax, xMin, xMax)
      lerp(cy, cyMin, cyMax, yMin, yMax)
    ]
  fromLocal = ([x, y]) ->
    [
      lerp(x, xMin, xMax, cxMin, cxMax)
      lerp(y, yMin, yMax, cyMin, cyMax)
    ]

  labelDistance = 5
  color = config.gridColor
  minorOpacity = 0.075
  majorOpacity = 0.1
  axesOpacity = 0.25
  labelOpacity = 1.0
  textHeight = 12

  minorColor = "rgba(#{color}, #{minorOpacity})"
  majorColor = "rgba(#{color}, #{majorOpacity})"
  axesColor = "rgba(#{color}, #{axesOpacity})"
  labelColor = "rgba(#{color}, #{labelOpacity})"

  ctx.save()
  ctx.lineWidth = 1

  # draw minor grid lines
  ctx.strokeStyle = minorColor
  for x in ticks(smallSpacing, xMin, xMax)
    drawLine(ctx, fromLocal([x, yMin]), fromLocal([x, yMax]))
  for y in ticks(smallSpacing, yMin, yMax)
    drawLine(ctx, fromLocal([xMin, y]), fromLocal([xMax, y]))

  # draw major grid lines
  ctx.strokeStyle = majorColor
  for x in ticks(largeSpacing, xMin, xMax)
    drawLine(ctx, fromLocal([x, yMin]), fromLocal([x, yMax]))
  for y in ticks(largeSpacing, yMin, yMax)
    drawLine(ctx, fromLocal([xMin, y]), fromLocal([xMax, y]))

  # draw axes
  ctx.strokeStyle = axesColor
  drawLine(ctx, fromLocal([0, yMin]), fromLocal([0, yMax]))
  drawLine(ctx, fromLocal([xMin, 0]), fromLocal([xMax, 0]))

  # draw labels
  labelEdgeDistance = labelDistance * 6 # To keep it from overlapping the axis labels
  ctx.font = "#{textHeight * devicePixelRatio}px verdana" # Scale font size for HiDPI displays
  ctx.fillStyle = labelColor
  ctx.textAlign = "center"
  ctx.textBaseline = "top"
  for x in ticks(largeSpacing, xMin, xMax)
    if x != 0
      text = parseFloat(x.toPrecision(12)).toString()
      [cx, cy] = fromLocal([x, 0])
      if cx < cxMax - labelEdgeDistance
        cy += labelDistance
        if cy < labelDistance
          cy = labelDistance
        if cy + textHeight + labelDistance > height
          cy = height - labelDistance - textHeight
        ctx.fillText(text, cx, cy)
  ctx.textAlign = "left"
  ctx.textBaseline = "middle"
  for y in ticks(largeSpacing, yMin, yMax)
    if y != 0
      text = parseFloat(y.toPrecision(12)).toString()
      [cx, cy] = fromLocal([0, y])
      if cy > labelEdgeDistance
        cx += labelDistance
        if cx < labelDistance
          cx = labelDistance
        if cx + ctx.measureText(text).width + labelDistance > width
          cx = width - labelDistance - ctx.measureText(text).width
        ctx.fillText(text, cx, cy)

  # draw axis labels
  # TODO: The axis labels overlap if you're in the lower left quadrant.
  if xLabel
    [originX, originY] = fromLocal([0, 0])

    text = xLabel
    ctx.fillStyle = xLabelColor
    cx = cxMax - labelDistance
    cy = originY + labelDistance
    if cy < labelDistance
      cy = labelDistance
    if cy + textHeight + labelDistance > height
      cy = height - labelDistance - textHeight
    ctx.textAlign = "right"
    ctx.textBaseline = "top"
    ctx.fillText(text, cx, cy)

    text = yLabel
    ctx.fillStyle = yLabelColor
    cx = originX + labelDistance
    cy = labelDistance
    if cx < labelDistance
      cx = labelDistance
    if cx + ctx.measureText(text).width + labelDistance > width
      cx = width - labelDistance - ctx.measureText(text).width
    ctx.textAlign = "left"
    ctx.textBaseline = "top"
    ctx.fillText(text, cx, cy)

  ctx.restore()

util.canvas = {
  lerp
  clear
  getSpacing
  drawGrid
}


text:
num = numeric

# Given a vector a, return a unit vector in the same direction as a.
normalize = (a) ->
  return num.div(a, num.norm2(a))

# Given a unit vector n, return a matrix which performs the reflection in the
# hyperplane perpendicular to n.
reflectionMatrix = (n) ->
  # http://en.wikipedia.org/wiki/Householder_transformation
  # http://en.wikipedia.org/wiki/Transformation_matrix#Reflection_2
  I = num.identity(n.length)
  return num.sub(I, num.mul(2, num.dot(num.transpose([n]), [n])))

# Given unit vectors a and b, return a matrix which performs the simplest
# (most direct) rotation that takes a to b.
rotationMatrix = (a, b) ->
  # Geometric Algebra for Physicists pg 47, fig 2.10

  # n is the unit vector between a and b
  n = normalize(num.add(a, b))

  # First reflect perpendicular to n, then reflect perpendicular to b
  Rn = reflectionMatrix(n)
  Rb = reflectionMatrix(b)
  return num.dot(Rb, Rn)

# Given vectors a and b, return a matrix which performs the simplest rotation
# and scaling that takes a to b.
rotationScalingMatrix = (a, b) ->
  aUnit = normalize(a)
  bUnit = normalize(b)

  scaleFactor = num.norm2(b) / num.norm2(a)
  I = num.identity(a.length)
  S = num.mul(scaleFactor, I)

  R = rotationMatrix(aUnit, bUnit)
  return num.dot(S, R)


text:
get = ->
  selection = window.getSelection()
  if selection.rangeCount > 0
    range = selection.getRangeAt(0)
    return range
  else
    return null

set = (range) ->
  selection = window.getSelection()
  if !range?
    # focusBody()
    selection.removeAllRanges()
  else
    # Focusing the host is necessary in FF for the text cursor to show up.
    host = findEditingHost(range.commonAncestorContainer)
    host?.focus()

    selection.removeAllRanges()
    selection.addRange(range)

getHost = ->
  selectedRange = get()
  return null unless selectedRange
  return findEditingHost(selectedRange.commonAncestorContainer)

beforeSelection = ->
  selectedRange = get()
  return null unless selectedRange
  host = getHost()
  range = document.createRange()
  range.selectNodeContents(host)
  range.setEnd(selectedRange.startContainer, selectedRange.startOffset)
  return range

afterSelection = ->
  selectedRange = get()
  return null unless selectedRange
  host = getHost()
  range = document.createRange()
  range.selectNodeContents(host)
  range.setStart(selectedRange.endContainer, selectedRange.endOffset)
  return range

isAtStart = ->
  return false unless get()?.collapsed
  return beforeSelection().toString() == ""

isAtEnd = ->
  return false unless get()?.collapsed
  return afterSelection().toString() == ""

setAtStart = (el) ->
  range = document.createRange()
  range.selectNodeContents(el)
  range.collapse(true)
  set(range)

setAtEnd = (el) ->
  range = document.createRange()
  range.selectNodeContents(el)
  range.collapse(false)
  set(range)

setAll = (el) ->
  range = document.createRange()
  range.selectNodeContents(el)
  set(range)

focusBody = ->
  body = document.body
  if !body.hasAttribute("tabindex")
    body.setAttribute("tabindex", "0")
  body.focus()

findEditingHost = (node) ->
  return null if !node?
  return findEditingHost(node.parentNode) if node.nodeType != Node.ELEMENT_NODE

  return null if !node.isContentEditable
  return node if !node.parentNode.isContentEditable

  return findEditingHost(node.parentNode)

util.selection = {
  get
  set
  getHost
  beforeSelection
  afterSelection
  isAtStart
  isAtEnd
  setAtStart
  setAtEnd
  setAll
}


text:
util.vector = vector = {}

zipWith = (f, a, b) ->
  result = []
  for aItem, index in a
    bItem = b[index]
    result.push(f(aItem, bItem))
  return result

add = (x, y) ->
  return null unless x? and y?
  return x + y

sub = (x, y) ->
  return null unless x? and y?
  return x - y

mul = (x, y) ->
  return null unless x? and y?
  return x * y

vector.add = (a, b) ->
  zipWith(add, a, b)

vector.sub = (a, b) ->
  zipWith(sub, a, b)

merge = (original, extension) ->
  if extension? then extension else original

vector.merge = (original, extension) ->
  zipWith(merge, original, extension)

vector.mul = (scalar, a) ->
  result = []
  for aItem in a
    result.push(mul(scalar, aItem))
  return result

vector.quadrance = (a) ->
  result = 0
  for aItem in a
    if aItem?
      result += aItem * aItem
  return result


text:
evaluate = (jsString) ->
  eval(jsString)

evaluateFn = (exprString) ->
  evaluate("(function (x) { return #{@exprString}; })")

identity = (a) -> a

add = (a, b) -> a + b
sub = (a, b) -> a - b
mul = (a, b) -> a * b
div = (a, b) -> a / b

abs = Math.abs
fract = (a) -> a - Math.floor(a)
floor = Math.floor
ceil = Math.ceil

min = Math.min
max = Math.max

sin = Math.sin
cos = Math.cos
sqrt = Math.sqrt
pow = (a, b) -> Math.pow(Math.abs(a), b)

PI = Math.PI
TAU = Math.PI * 2

util.evaluate = evaluate
util.evaluateFn = evaluateFn