/*
Copyright 2020-2023, Savanni D'Gerinel <savanni@luminescent-dreams.com>

This file is part of the Luminescent Dreams Tools.

Luminescent Dreams Tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

Luminescent Dreams Tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with Lumeto. If not, see <https://www.gnu.org/licenses/>.
*/

use date_time_tz::DateTimeTz;
use types::{Recordable, Timestamp};

/// This trait is used for constructing queries for searching the database.
pub trait Criteria {
    /// Apply this criteria element to a record, returning true only if the record matches the
    /// criteria.
    fn apply<T: Recordable>(&self, record: &T) -> bool;
}

/// Specify two criteria that must both be matched.
pub struct And<A: Criteria, B: Criteria> {
    pub lside: A,
    pub rside: B,
}

impl<A, B> Criteria for And<A, B>
where
    A: Criteria,
    B: Criteria,
{
    fn apply<T: Recordable>(&self, record: &T) -> bool {
        self.lside.apply(record) && self.rside.apply(record)
    }
}

/// Specify two criteria, either of which may be matched.
pub struct Or<A: Criteria, B: Criteria> {
    pub lside: A,
    pub rside: B,
}

/// Specify the starting time for a search. This consists of a UTC timestamp and a specifier as to
/// whether the exact time is included in the search criteria.
pub struct StartTime {
    pub time: Timestamp,
    pub incl: bool,
}

impl Criteria for StartTime {
    fn apply<T: Recordable>(&self, record: &T) -> bool {
        if self.incl {
            record.timestamp() >= self.time
        } else {
            record.timestamp() > self.time
        }
    }
}

/// Specify the ending time for a search. This consists of a UTC timestamp and a specifier as to
/// whether the exact time is included in the search criteria.
pub struct EndTime {
    pub time: Timestamp,
    pub incl: bool,
}

impl Criteria for EndTime {
    fn apply<T: Recordable>(&self, record: &T) -> bool {
        if self.incl {
            record.timestamp() <= self.time
        } else {
            record.timestamp() < self.time
        }
    }
}

/// Specify a list of tags that must exist on the record.
pub struct Tags {
    pub tags: Vec<String>,
}

impl Criteria for Tags {
    fn apply<T: Recordable>(&self, record: &T) -> bool {
        let record_tags = record.tags();
        self.tags.iter().all(|v| record_tags.contains(v))
    }
}

/// Specify a criteria that searches for records matching an exact time.
pub fn exact_time(time: Timestamp) -> And<StartTime, EndTime> {
    And {
        lside: StartTime {
            time: time.clone(),
            incl: true,
        },
        rside: EndTime { time, incl: true },
    }
}

/// Specify a criteria that searches for all records within a time range.
pub fn time_range(
    start: Timestamp,
    start_incl: bool,
    end: Timestamp,
    end_incl: bool,
) -> And<StartTime, EndTime> {
    And {
        lside: StartTime {
            time: start,
            incl: start_incl,
        },
        rside: EndTime {
            time: end,
            incl: end_incl,
        },
    }
}