#![allow(unused_imports)]

// language: Rust(1.42.0)
// check available crates on AtCoder at "https://atcoder.jp/contests/language-test-202001"
// My Library Repositry is at "https://github.com/Loptall/sfcpl"

/*
    水コーダーになれてうれしいです
*/

// from std...
use std::cmp::{
    max, min, Ordering,
    Ordering::{Equal, Greater, Less},
    Reverse,
};
use std::collections::{BTreeMap, BTreeSet, BinaryHeap, HashMap, HashSet, VecDeque};
use std::convert::TryInto;
use std::fmt;
use std::mem::swap;
use std::num::{NonZeroU32, ParseIntError};
use std::ops::{
    Add, AddAssign, BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign, Div, DivAssign,
    Index, IndexMut, Mul, MulAssign, Neg, Rem, RemAssign, Shl, ShlAssign, Shr, ShrAssign, Sub,
    SubAssign,
};
use std::process::exit;
use std::{f32, f64, i128, i16, i32, i64, i8, isize, u128, u16, u32, u64, u8, usize};

// Dep' crates are...
use itertools::*;
use itertools_num::*;
use lazy_static::lazy_static;
use maplit::{btreemap, btreeset, hashmap, hashset};
use num_bigint::{BigInt, BigUint};
use num_complex::Complex;
use num_integer::{binomial, gcd, lcm, multinomial, Integer};
use num_rational::Rational;
use num_traits::{
    clamp, one, pow, zero, Num, NumAssignOps, NumOps, One, Pow, Signed, Unsigned, Zero,
};
use permutohedron::Heap;
use proconio::{
    derive_readable, fastout, input, is_stdin_empty,
    marker::{Bytes, Chars, Isize1, Usize1},
};
use rand::random;

pub trait Visualize {
    fn visualize(&self, split: &str);
    fn continuous(&self) {
        self.visualize("");
    }
    fn spaces(&self) {
        self.visualize(" ");
    }
    fn lines(&self) {
        self.visualize("\n");
    }
}
macro_rules! impl_vis_for_sized {
    ($($t:ty),+) => {
        $(
            impl Visualize for $t {
                fn visualize(&self, _split: &str) {
                    print!("{}", self);
                }
            }
        )+
    };
}
impl_vis_for_sized! {
    usize, u8, u16, u32, u64, u128,
    isize, i8, i16, i32, i64, i128,
    f64, f32,
    String, &str, char
}
impl<T: fmt::Display> Visualize for [T] {
    fn visualize(&self, split: &str) {
        print!("{}", self.iter().join(split));
    }
}
#[macro_export]
macro_rules! vis {
    // end
    () => {
        println!();
    };

    // last element + trailing pattern
    ($last:expr ;) => {
        $last.lines();
        vis!()
    };
    ($last:expr =>) => {
        $last.continuous();
        vis!();
    };
    ($last:expr $(,)?) => {
        $last.spaces();
        vis!();
    };

    // get first element and pass rest
    ($first:expr; $($rest:tt)*) => {
        $first.lines();
        println!();
        vis!($($rest)*);
    };
    ($first:expr => $($rest:tt)*) => {
        $first.continuous();
        vis!($($rest)*);
    };
    ($first:expr, $($rest:tt)*) => {
        $first.spaces();
        print!(" ");
        vis!($($rest)*);
    };
}
pub const MOD10E9_7: usize = 1000000007; // 10 ^ 9 + 7
pub const MOD99_: usize = 998244353;
pub const MAX: usize = std::usize::MAX; // = 2 ^ 64 - 1 = 18446744073709551615 ≈ 1.8 * 10 ^ 19
pub const INF: usize = 2000000000000000000; // MAX / 9 < 2 * 10e18 < MAX / 10
pub const FNI: i64 = -2000000000000000000; // == -(INF as i64)
pub const PI: f64 = f64::consts::PI; // 3.141592653589793 -- 10 ^ -15
pub const ASCII_A_LARGE: u8 = 65;
pub const ASCII_A_SMALL: u8 = 97;
pub const ASCII_0: u8 = 48;
pub const ADJ4: &[(isize, isize); 4] = &[(1, 0), (0, 1), (-1, 0), (0, -1)];
pub const ADJ8: &[(isize, isize); 8] = &[
    (1, 0),
    (1, 1),
    (0, 1),
    (-1, 1),
    (-1, 0),
    (-1, -1),
    (0, -1),
    (1, -1),
];

type Idx2D = (usize, usize);

const N4: [(isize, isize); 4] = [(1, 0), (0, 1), (-1, 0), (0, -1)];
const N8: [(isize, isize); 8] = [
    (1, 0),
    (1, 1),
    (0, 1),
    (-1, 1),
    (-1, 0),
    (-1, -1),
    (0, -1),
    (1, -1),
];

#[derive(Debug, Clone)]
pub struct Grid<'a> {
    h: usize,
    w: usize,
    board: &'a Vec<Vec<char>>,
    wall: HashSet<char>,
}

impl<'a> Grid<'a> {
    pub fn new(board: &'a Vec<Vec<char>>, wall: Vec<char>) -> Self {
        Self {
            h: board.len(),
            w: board[0].len(),
            board,
            wall: wall.into_iter().collect(),
        }
    }

    pub fn height(&self) -> usize {
        self.h
    }

    pub fn width(&self) -> usize {
        self.w
    }

    pub fn index(&self, idx: Idx2D) -> char {
        self.board[idx.0][idx.1]
    }

    pub fn is_passable(&self, idx: Idx2D) -> bool {
        !self.wall.contains(&self.index(idx))
    }

    pub fn neighbor4(&self, idx: Idx2D) -> impl Iterator<Item = Idx2D> {
        let mut res = Vec::new();
        for &(x, y) in N4.iter() {
            let idx = ((idx.0 as isize) + x, (idx.1 as isize) + y);
            if idx.0 < 0
                || idx.0 >= self.height() as isize
                || idx.1 < 0
                || idx.1 >= self.width() as isize
            {
                continue;
            }
            res.push((idx.0 as usize, idx.1 as usize));
        }
        res.into_iter()
    }

    pub fn neighbor8(&self, idx: Idx2D) -> impl Iterator<Item = Idx2D> {
        let mut res = Vec::new();
        for &(x, y) in N8.iter() {
            let idx = ((idx.0 as isize) + x, (idx.1 as isize) + y);
            if idx.0 < 0
                || idx.0 >= self.height() as isize
                || idx.1 < 0
                || idx.1 >= self.width() as isize
            {
                continue;
            }
            let idx = (idx.0 as usize, idx.1 as usize);
            if self.is_passable(idx) {
                res.push(idx);
            }
        }
        res.into_iter()
    }

    pub fn find(&self, c: char) -> Option<Idx2D> {
        for i in 0..self.height() {
            for j in 0..self.width() {
                if self.index((i, j)) == c {
                    return Some((i, j));
                }
            }
        }
        None
    }

    pub fn rfind(&self, c: char) -> Option<Idx2D> {
        for i in (0..self.height()).rev() {
            for j in (0..self.width()).rev() {
                if self.index((i, j)) == c {
                    return Some((i, j));
                }
            }
        }
        None
    }
}

pub struct Dfs<'a> {
    visited: Vec<Vec<bool>>,
    s: Vec<(Idx2D, Option<Idx2D>)>,
    g: &'a Grid<'a>,
}

impl<'a> Iterator for Dfs<'a> {
    type Item = (Idx2D, Idx2D);
    fn next(&mut self) -> Option<Self::Item> {
        if let Some((u, prev)) = self.s.pop() {
            if !self.g.is_passable(u) {
                return self.next();
            }

            for v in self.g.neighbor4(u) {
                if !self.visited[v.0][v.1] {
                    self.visited[v.0][v.1] = true;
                    self.s.push((v, Some(u)));
                }
            }

            if let Some(prev) = prev {
                Some((prev, u))
            } else {
                self.next()
            }
        } else {
            None
        }
    }
}

pub fn dfs<'a>(g: &'a Grid, start: Idx2D) -> Dfs<'a> {
    let mut visited = vec![vec![false; g.width()]; g.height()];
    visited[start.0][start.1] = true;
    let mut s = Vec::new();
    s.push((start, None));
    Dfs { visited, s, g }
}

pub struct Bfs<'a> {
    visited: Vec<Vec<bool>>,
    q: VecDeque<(Idx2D, Option<Idx2D>)>,
    g: &'a Grid<'a>,
}

impl<'a> Iterator for Bfs<'a> {
    type Item = (Idx2D, Idx2D);
    fn next(&mut self) -> Option<Self::Item> {
        if let Some((u, prev)) = self.q.pop_front() {
            if !self.g.is_passable(u) {
                return self.next();
            }

            for v in self.g.neighbor4(u) {
                if !self.visited[v.0][v.1] {
                    self.visited[v.0][v.1] = true;
                    self.q.push_back((v, Some(u)));
                }
            }

            if let Some(prev) = prev {
                Some((prev, u))
            } else {
                self.next()
            }
        } else {
            None
        }
    }
}

pub fn bfs<'a>(g: &'a Grid, start: Idx2D) -> Bfs<'a> {
    let mut visited = vec![vec![false; g.width()]; g.height()];
    visited[start.0][start.1] = true;
    let mut q = VecDeque::new();
    q.push_back((start, None));
    Bfs { visited, q, g }
}

#[test]
fn dfs_test() {
    let b = vec![
        vec!['.', '.', '#'],
        vec!['.', '.', '.'],
        vec!['#', '.', '.'],
    ];
    let g = Grid::new(&b, vec!['#']);
    for (a, b) in dfs(&g, (0, 0)) {
        dbg!(a, b);
    }
    // panic!()
}

#[test]
fn bfs_test() {
    let b = vec![
        vec!['.', '.', '#'],
        vec!['.', '.', '.'],
        vec!['#', '.', '.'],
    ];
    let g = Grid::new(&b, vec!['#']);
    for (a, b) in bfs(&g, (0, 0)) {
        dbg!(a, b);
    }
    // panic!()
}

pub fn is_joint(g: &Grid, from: Idx2D, to: Idx2D) -> bool {
    let mut used = vec![vec![false; g.width()]; g.height()];
    is_joint_inner(g, from, to, &mut used)
}

pub fn is_joint_inner(g: &Grid, from: Idx2D, to: Idx2D, used: &mut Vec<Vec<bool>>) -> bool {
    used[from.0][from.1] = true;
    dbg!(from);
    if from == to {
        return true;
    }
    let iter = g
        .neighbor4(from)
        .filter(|x| !used[x.0][x.1] && g.is_passable(*x))
        .collect::<Vec<Idx2D>>();
    for &(x, y) in iter.iter() {
        if is_joint_inner(g, (x, y), to, used) {
            return true;
        }
    }

    false
}

#[test]
fn grid_joint_test() {
    let b = vec![
        vec!['.', '.', '#'],
        vec!['#', '.', '#'],
        vec!['#', '.', '.'],
    ];
    let g = Grid::new(&b, vec!['#']);
    assert!(is_joint(&g, (0, 0), (2, 2)));

    let b = vec![
        vec!['.', '.', '#'],
        vec!['#', '#', '#'],
        vec!['#', '.', '.'],
    ];
    let g = Grid::new(&b, vec!['#']);
    assert!(!is_joint(&g, (0, 0), (2, 2)));
}

pub fn dist_table(g: &Grid, start: Idx2D) -> Vec<Vec<usize>> {
    let mut res = vec![vec![std::usize::MAX; g.width()]; g.height()];
    res[start.0][start.1] = 0;
    for (a, b) in bfs(&g, start) {
        res[b.0][b.1] = res[a.0][a.1] + 1;
    }

    res
}

#[test]
fn dist_test() {
    let b = vec![
        vec!['.', '.', '#'],
        vec!['.', '.', '.'],
        vec!['#', '.', '.'],
    ];
    let g = Grid::new(&b, vec!['#']);

    let d = dist_table(&g, (0, 0));
    assert_eq!(
        d,
        vec![
            vec![0, 1, std::usize::MAX],
            vec![1, 2, 3],
            vec![std::usize::MAX, 3, 4]
        ]
    );
}

// code...
// #[fastout]
fn main() {
    input! {
        h: usize, w: usize,
        s: [Chars; h]
    }

    let g = Grid::new(&s, vec!['#']);

    dbg!(&g);

    let mut ans = 0;
    for si in 0..h {
        for sj in 0..w {
            if g.board[si][sj] == '.' {
                let d = dist_table(&g, (si, sj));
                ans = max(
                    ans,
                    d.into_iter().flatten().filter(|x| *x < INF).max().unwrap(),
                );
            }
        }
    }

    println!("{}", ans);
}


Measured memory_bytes: 2248