use std::str::FromStr;
use std::io::*;
use std::collections::*;
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(c: &mut [(Tile, usize)], cts: &[(Tile, usize); 4], n: usize) -> usize {
        if n == 4 {
            return 1;
        }
        let ct = cts[n];
        let mut result = 0;
        for i in 0..c.len() {
            if c[i].1 == 0 || c[i].0 != ct.0 {
                continue;
            }
            let count = c[i].1 * ct.1;
            c[i].1 -= 1;
            result += count * f(c, cts, n + 1);
            c[i].1 += 1;
        }
        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(0) += 1;
    }
    let mut c: Vec<_> = m.into_iter().collect();
    let mut result = 0;
    let mut cts = [([0; 4], 0); 4];
    for i0 in 0..c.len() {
        let c0 = c[i0];
        c[i0].1 -= 1;
        for i1 in 0..c.len() {
            let mut c1 = c[i1];
            if c1.1 == 0 {
                continue;
            }
            c1.0.reverse();
            c[i1].1 -= 1;
            for _ in 0..4 {
                for j0 in 0..4 {
                    let j1 = (j0 + 1) % 4;
                    cts[j0] = normalize([c0.0[j0], c1.0[j0], c1.0[j1], c0.0[j1]]);
                }
                result += c0.1 * c1.1 * f(&mut c, &cts, 0);
                c1.0.rotate(1);
            }
            c[i1].1 += 1;
        }
        c[i0].1 += 1;
    }
    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