use std::str::FromStr;
use std::io::*;
use std::collections::*;
use std::cell::Cell;
use utils::*;

pub fn main() {
    let i = stdin();
    let mut o = Vec::new();
    run(i.lock(), &mut o);
    stdout().write_all(&o).unwrap();
}
type Tile = [usize; 4];
fn run<R: BufRead, W: Write>(i: R, o: &mut W) {
    let mut i = AtRead::from(i);
    let n = i.read::<usize>();
    let mut c = Vec::new();
    for _ in 0..n {
        c.push(i.read_array::<[usize; 4], _>());
    }
    writeln!(o, "{}", solve(c)).unwrap();
}

fn solve(c: Vec<Tile>) -> usize {
    fn f(m: &HashMap<Tile, Cell<usize>>, cts: &[(Tile, usize); 4], n: usize) -> usize {
        if n == 4 {
            return 1;
        }
        let mut result = 0;
        if let Some(count_cell) = m.get(&cts[n].0) {
            let count = count_cell.get();
            if count != 0 {
                count_cell.set(count - 1);
                result = cts[n].1 * count * f(m, cts, n + 1);
                count_cell.set(count);
            }
        }
        result
    }
    fn normalize(mut c: Tile) -> (Tile, usize) {
        let mut c_min = c;
        let mut count = 1;
        for _ in 1..4 {
            c.rotate(1);
            if c < c_min {
                c_min = c;
                count = 1;
            } else if c == c_min {
                count += 1;
            }
        }
        (c_min, count)
    }
    let mut m = HashMap::new();
    for &c in &c {
        *m.entry(normalize(c).0).or_insert(Cell::new(0)).get_mut() += 1;
    }
    let mut result = 0;
    let mut cts = [([0; 4], 0); 4];
    for c0 in &m {
        let c0_count = c0.1.get();
        c0.1.set(c0_count - 1);
        for c1 in &m {
            let c1_count = c1.1.get();
            if c1_count == 0 {
                continue;
            }
            c1.1.set(c1_count - 1);
            let mut c1c = *c1.0;
            c1c.reverse();
            for _ in 0..4 {
                for j0 in 0..4 {
                    let j1 = (j0 + 1) % 4;
                    cts[j0] = normalize([c0.0[j0], c1c[j0], c1c[j1], c0.0[j1]]);
                }
                result += c0_count * c1_count * f(&m, &cts, 0);
                c1c.rotate(1);
            }
            c1.1.set(c1_count);
        }
        c0.1.set(c0_count);
    }
    result / 6
}

mod utils {
    use super::*;

    pub struct AtRead<R: BufRead> {
        r: R,
        s: String,
    }

    impl<R: BufRead> AtRead<R> {
        pub fn from(r: R) -> Self {
            AtRead {
                r: r,
                s: String::new(),
            }
        }
        pub fn read_line(&mut self) -> &str {
            self.s.clear();
            self.r.read_line(&mut self.s).unwrap();
            self.s.trim()
        }

        pub fn read<T: FromStr>(&mut self) -> T {
            self.read_line().parse().ok().unwrap()
        }

        pub fn read_array<A, T>(&mut self) -> A
        where
            T: FromStr,
            A: AsMut<[T]> + Default,
        {
            let mut a: A = Default::default();
            {
                let a: &mut [T] = a.as_mut();
                let i = &mut self.read_line().split(' ');
                for n in 0..a.len() {
                    a[n] = next(i);
                }
            }
            a
        }
    }

    fn next<'a, T: FromStr, I: std::iter::Iterator<Item = &'a str>>(i: &mut I) -> T {
        i.next().unwrap().parse().ok().unwrap()
    }

    pub trait AtSliceExt<T> {
        fn rotate(&mut self, mid: usize);
    }
    impl<T> AtSliceExt<T> for [T] {
        fn rotate(&mut self, mid: usize) {
            {
                let (l, r) = self.split_at_mut(mid);
                l.reverse();
                r.reverse();
            }
            self.reverse()
        }
    }
}


Measured memory_bytes: 4352